diff --git a/bazel/BUILD b/bazel/BUILD index 11118ff8caabf..8c0c4224bae59 100644 --- a/bazel/BUILD +++ b/bazel/BUILD @@ -318,6 +318,11 @@ selects.config_setting_group( ], ) +config_setting( + name = "disable_admin_html", + values = {"define": "admin_html=disabled"}, +) + config_setting( name = "disable_hot_restart_setting", values = {"define": "hot_restart=disabled"}, diff --git a/bazel/README.md b/bazel/README.md index 042e83a34adcd..2f60fc0dc04df 100644 --- a/bazel/README.md +++ b/bazel/README.md @@ -661,6 +661,7 @@ The following optional features can be disabled on the Bazel build command-line: tcmalloc with `--define tcmalloc=gperftools` which is the default for builds other than x86_64 and aarch64. * deprecated features with `--define deprecated_features=disabled` * http3/quic with --//bazel:http3=False +* admin HTML home page with `--define=admin_html=disabled` ## Enabling optional features diff --git a/bazel/envoy_build_system.bzl b/bazel/envoy_build_system.bzl index f48ebe70564e9..f87903151df8d 100644 --- a/bazel/envoy_build_system.bzl +++ b/bazel/envoy_build_system.bzl @@ -18,6 +18,8 @@ load( load(":envoy_pch.bzl", _envoy_pch_library = "envoy_pch_library") load( ":envoy_select.bzl", + _envoy_select_admin_html = "envoy_select_admin_html", + _envoy_select_admin_no_html = "envoy_select_admin_no_html", _envoy_select_boringssl = "envoy_select_boringssl", _envoy_select_enable_http3 = "envoy_select_enable_http3", _envoy_select_google_grpc = "envoy_select_google_grpc", @@ -206,6 +208,8 @@ def envoy_google_grpc_external_deps(): # from the other bzl files (e.g. envoy_select.bzl, envoy_binary.bzl, etc.) # Select wrappers (from envoy_select.bzl) +envoy_select_admin_html = _envoy_select_admin_html +envoy_select_admin_no_html = _envoy_select_admin_no_html envoy_select_boringssl = _envoy_select_boringssl envoy_select_google_grpc = _envoy_select_google_grpc envoy_select_enable_http3 = _envoy_select_enable_http3 diff --git a/bazel/envoy_internal.bzl b/bazel/envoy_internal.bzl index f2c15f6e0ba1c..76bce7f05a71f 100644 --- a/bazel/envoy_internal.bzl +++ b/bazel/envoy_internal.bzl @@ -1,6 +1,6 @@ # DO NOT LOAD THIS FILE. Targets from this file should be considered private # and not used outside of the @envoy//bazel package. -load(":envoy_select.bzl", "envoy_select_enable_http3", "envoy_select_google_grpc", "envoy_select_hot_restart") +load(":envoy_select.bzl", "envoy_select_admin_html", "envoy_select_enable_http3", "envoy_select_google_grpc", "envoy_select_hot_restart") # Compute the final copts based on various options. def envoy_copts(repository, test = False): @@ -122,6 +122,7 @@ def envoy_copts(repository, test = False): repository + "//bazel:uhv_enabled": ["-DENVOY_ENABLE_UHV"], "//conditions:default": [], }) + envoy_select_hot_restart(["-DENVOY_HOT_RESTART"], repository) + \ + envoy_select_admin_html(["-DENVOY_ADMIN_HTML"], repository) + \ envoy_select_enable_http3(["-DENVOY_ENABLE_QUIC"], repository) + \ _envoy_select_perf_annotation(["-DENVOY_PERF_ANNOTATION"]) + \ _envoy_select_perfetto(["-DENVOY_PERFETTO"]) + \ diff --git a/bazel/envoy_select.bzl b/bazel/envoy_select.bzl index 4e664fe661868..50c34b8388b73 100644 --- a/bazel/envoy_select.bzl +++ b/bazel/envoy_select.bzl @@ -25,6 +25,19 @@ def envoy_select_google_grpc(xs, repository = ""): "//conditions:default": xs, }) +# Selects the given values if admin HTML is enabled in the current build. +def envoy_select_admin_html(xs, repository = ""): + return select({ + repository + "//bazel:disable_admin_html": [], + "//conditions:default": xs, + }) + +def envoy_select_admin_no_html(xs, repository = ""): + return select({ + repository + "//bazel:disable_admin_html": xs, + "//conditions:default": [], + }) + # Selects the given values if http3 is enabled in the current build. def envoy_select_enable_http3(xs, repository = ""): return select({ diff --git a/changelogs/current.yaml b/changelogs/current.yaml index f2f46e75a7f14..48e92fac53d43 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -57,8 +57,11 @@ minor_behavior_changes: that loads shared object libraries, such as those installed via luarocks. - area: admin change: | - changed default regex engine for /stats?filter= from std::regex to RE2, improving filtering speed - 20x. + changed default regex engine for ``/stats?filter=`` from std::regex to RE2, improving + filtering speed 20x. +- area: admin + change: | + added compile-time option ``--define=admin_html=disabled`` to disable HTML home page. - area: skywalking change: | use request path as operation name of ENTRY/EXIT spans. diff --git a/ci/do_ci.sh b/ci/do_ci.sh index be00402efa65b..d85ae4bb80863 100755 --- a/ci/do_ci.sh +++ b/ci/do_ci.sh @@ -366,6 +366,7 @@ elif [[ "$CI_TARGET" == "bazel.compile_time_options" ]]; then # Right now, none of the available compile-time options conflict with each other. If this # changes, this build type may need to be broken up. COMPILE_TIME_OPTIONS=( + "--define" "admin_html=disabled" "--define" "signal_trace=disabled" "--define" "hot_restart=disabled" "--define" "google_grpc=disabled" diff --git a/source/server/admin/BUILD b/source/server/admin/BUILD index 69371bbb6c781..bbc3c475f76fe 100644 --- a/source/server/admin/BUILD +++ b/source/server/admin/BUILD @@ -2,6 +2,8 @@ load( "//bazel:envoy_build_system.bzl", "envoy_cc_library", "envoy_package", + "envoy_select_admin_html", + "envoy_select_admin_no_html", ) licenses(["notice"]) # Apache 2 @@ -10,7 +12,11 @@ envoy_package() envoy_cc_library( name = "admin_lib", - srcs = ["admin.cc"], + srcs = ["admin.cc"] + envoy_select_admin_html([ + "admin_html.cc", + ]) + envoy_select_admin_no_html([ + "admin_no_html.cc", + ]), hdrs = ["admin.h"], deps = [ ":admin_filter_lib", @@ -45,7 +51,6 @@ envoy_cc_library( "//source/common/common:mutex_tracer_lib", "//source/common/common:utility_lib", "//source/common/formatter:substitution_formatter_lib", - "//source/common/html:utility_lib", "//source/common/http:codes_lib", "//source/common/http:conn_manager_lib", "//source/common/http:date_provider_lib", diff --git a/source/server/admin/admin.cc b/source/server/admin/admin.cc index 9efe33c00a191..fe2dc58f5dde4 100644 --- a/source/server/admin/admin.cc +++ b/source/server/admin/admin.cc @@ -23,7 +23,6 @@ #include "source/common/common/mutex_tracer_impl.h" #include "source/common/common/utility.h" #include "source/common/formatter/substitution_formatter.h" -#include "source/common/html/utility.h" #include "source/common/http/codes.h" #include "source/common/http/conn_manager_utility.h" #include "source/common/http/header_map_impl.h" @@ -45,72 +44,6 @@ namespace Envoy { namespace Server { -namespace { - -/** - * Favicon base64 image was harvested by screen-capturing the favicon from a Chrome tab - * while visiting https://www.envoyproxy.io/. The resulting PNG was translated to base64 - * by dropping it into https://www.base64-image.de/ and then pasting the resulting string - * below. - * - * The actual favicon source for that, https://www.envoyproxy.io/img/favicon.ico is nicer - * because it's transparent, but is also 67646 bytes, which is annoying to inline. We could - * just reference that rather than inlining it, but then the favicon won't work when visiting - * the admin page from a network that can't see the internet. - */ -const char EnvoyFavicon[] = - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1" - "BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAH9SURBVEhL7ZRdTttAFIUrUFaAX5w9gIhgUfzshFRK+gIbaVbA" - "zwaqCly1dSpKk5A485/YCdXpHTB4BsdgVe0bD0cZ3Xsm38yZ8byTUuJ/6g3wqqoBrBhPTzmmLfptMbAzttJTpTKAF2MWC" - "7ADCdNIwXZpvMMwayiIwwS874CcOc9VuQPR1dBBChPMITpFXXU45hukIIH6kHhzVqkEYB8F5HYGvZ5B7EvwmHt9K/59Cr" - "U3QbY2RNYaQPYmJc+jPIBICNCcg20ZsAsCPfbcrFlRF+cJZpvXSJt9yMTxO/IAzJrCOfhJXiOgFEX/SbZmezTWxyNk4Q9" - "anHMmjnzAhEyhAW8LCE6wl26J7ZFHH1FMYQxh567weQBOO1AW8D7P/UXAQySq/QvL8Fu9HfCEw4SKALm5BkC3bwjwhSKr" - "A5hYAMXTJnPNiMyRBVzVjcgCyHiSm+8P+WGlnmwtP2RzbCMiQJ0d2KtmmmPorRHEhfMROVfTG5/fYrF5iWXzE80tfy9WP" - "sCqx5Buj7FYH0LvDyHiqd+3otpsr4/fa5+xbEVQPfrYnntylQG5VGeMLBhgEfyE7o6e6qYzwHIjwl0QwXSvvTmrVAY4D5" - "ddvT64wV0jRrr7FekO/XEjwuwwhuw7Ef7NY+dlfXpLb06EtHUJdVbsxvNUqBrwj/QGeEUSfwBAkmWHn5Bb/gAAAABJRU5"; - -const char AdminHtmlStart[] = R"( - - Envoy Admin - - - - - - - - - - -)"; - -const char AdminHtmlEnd[] = R"( - -
CommandDescription
- -)"; -} // namespace - ConfigTracker& AdminImpl::getConfigTracker() { return config_tracker_; } AdminImpl::NullRouteConfigProvider::NullRouteConfigProvider(TimeSource& time_source) @@ -389,53 +322,6 @@ void AdminImpl::getHelp(Buffer::Instance& response) { } } -Http::Code AdminImpl::handlerAdminHome(absl::string_view, Http::ResponseHeaderMap& response_headers, - Buffer::Instance& response, AdminStream&) { - response_headers.setReferenceContentType(Http::Headers::get().ContentTypeValues.Html); - - response.add(absl::StrReplaceAll(AdminHtmlStart, {{"@FAVICON@", EnvoyFavicon}})); - - // Prefix order is used during searching, but for printing do them in alpha order. - for (const UrlHandler* handler : sortedHandlers()) { - absl::string_view path = handler->prefix_; - - if (path == "/") { - continue; // No need to print self-link to index page. - } - - // Remove the leading slash from the link, so that the admin page can be - // rendered as part of another console, on a sub-path. - // - // E.g. consider a downstream dashboard that embeds the Envoy admin console. - // In that case, the "/stats" endpoint would be at - // https://DASHBOARD/envoy_admin/stats. If the links we present on the home - // page are absolute (e.g. "/stats") they won't work in the context of the - // dashboard. Removing the leading slash, they will work properly in both - // the raw admin console and when embedded in another page and URL - // hierarchy. - ASSERT(!path.empty()); - ASSERT(path[0] == '/'); - path = path.substr(1); - - // For handlers that mutate state, render the link as a button in a POST form, - // rather than an anchor tag. This should discourage crawlers that find the / - // page from accidentally mutating all the server state by GETting all the hrefs. - const char* link_format = - handler->mutates_server_state_ - ? "
" - : "{}"; - const std::string link = fmt::format(link_format, path, path); - - // Handlers are all specified by statically above, and are thus trusted and do - // not require escaping. - response.add(fmt::format("{}" - "{}\n", - link, Html::Utility::sanitize(handler->help_text_))); - } - response.add(AdminHtmlEnd); - return Http::Code::OK; -} - const Network::Address::Instance& AdminImpl::localAddress() { return *server_.localInfo().address(); } diff --git a/source/server/admin/admin_html.cc b/source/server/admin/admin_html.cc new file mode 100644 index 0000000000000..3745bf85538d4 --- /dev/null +++ b/source/server/admin/admin_html.cc @@ -0,0 +1,122 @@ +#include "source/common/html/utility.h" +#include "source/server/admin/admin.h" + +namespace Envoy { +namespace Server { + +namespace { + +/** + * Favicon base64 image was harvested by screen-capturing the favicon from a Chrome tab + * while visiting https://www.envoyproxy.io/. The resulting PNG was translated to base64 + * by dropping it into https://www.base64-image.de/ and then pasting the resulting string + * below. + * + * The actual favicon source for that, https://www.envoyproxy.io/img/favicon.ico is nicer + * because it's transparent, but is also 67646 bytes, which is annoying to inline. We could + * just reference that rather than inlining it, but then the favicon won't work when visiting + * the admin page from a network that can't see the internet. + */ +const char EnvoyFavicon[] = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1" + "BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAH9SURBVEhL7ZRdTttAFIUrUFaAX5w9gIhgUfzshFRK+gIbaVbA" + "zwaqCly1dSpKk5A485/YCdXpHTB4BsdgVe0bD0cZ3Xsm38yZ8byTUuJ/6g3wqqoBrBhPTzmmLfptMbAzttJTpTKAF2MWC" + "7ADCdNIwXZpvMMwayiIwwS874CcOc9VuQPR1dBBChPMITpFXXU45hukIIH6kHhzVqkEYB8F5HYGvZ5B7EvwmHt9K/59Cr" + "U3QbY2RNYaQPYmJc+jPIBICNCcg20ZsAsCPfbcrFlRF+cJZpvXSJt9yMTxO/IAzJrCOfhJXiOgFEX/SbZmezTWxyNk4Q9" + "anHMmjnzAhEyhAW8LCE6wl26J7ZFHH1FMYQxh567weQBOO1AW8D7P/UXAQySq/QvL8Fu9HfCEw4SKALm5BkC3bwjwhSKr" + "A5hYAMXTJnPNiMyRBVzVjcgCyHiSm+8P+WGlnmwtP2RzbCMiQJ0d2KtmmmPorRHEhfMROVfTG5/fYrF5iWXzE80tfy9WP" + "sCqx5Buj7FYH0LvDyHiqd+3otpsr4/fa5+xbEVQPfrYnntylQG5VGeMLBhgEfyE7o6e6qYzwHIjwl0QwXSvvTmrVAY4D5" + "ddvT64wV0jRrr7FekO/XEjwuwwhuw7Ef7NY+dlfXpLb06EtHUJdVbsxvNUqBrwj/QGeEUSfwBAkmWHn5Bb/gAAAABJRU5"; + +const char AdminHtmlStart[] = R"( + + Envoy Admin + + + + + + + + + + +)"; + +const char AdminHtmlEnd[] = R"( + +
CommandDescription
+ +)"; + +} // namespace + +Http::Code AdminImpl::handlerAdminHome(absl::string_view, Http::ResponseHeaderMap& response_headers, + Buffer::Instance& response, AdminStream&) { + response_headers.setReferenceContentType(Http::Headers::get().ContentTypeValues.Html); + + response.add(absl::StrReplaceAll(AdminHtmlStart, {{"@FAVICON@", EnvoyFavicon}})); + + // Prefix order is used during searching, but for printing do them in alpha order. + for (const UrlHandler* handler : sortedHandlers()) { + absl::string_view path = handler->prefix_; + + if (path == "/") { + continue; // No need to print self-link to index page. + } + + // Remove the leading slash from the link, so that the admin page can be + // rendered as part of another console, on a sub-path. + // + // E.g. consider a downstream dashboard that embeds the Envoy admin console. + // In that case, the "/stats" endpoint would be at + // https://DASHBOARD/envoy_admin/stats. If the links we present on the home + // page are absolute (e.g. "/stats") they won't work in the context of the + // dashboard. Removing the leading slash, they will work properly in both + // the raw admin console and when embedded in another page and URL + // hierarchy. + ASSERT(!path.empty()); + ASSERT(path[0] == '/'); + path = path.substr(1); + + // For handlers that mutate state, render the link as a button in a POST form, + // rather than an anchor tag. This should discourage crawlers that find the / + // page from accidentally mutating all the server state by GETting all the hrefs. + const char* link_format = + handler->mutates_server_state_ + ? "
" + : "{}"; + const std::string link = fmt::format(link_format, path, path); + + // Handlers are all specified by statically above, and are thus trusted and do + // not require escaping. + response.add(fmt::format("{}" + "{}\n", + link, Html::Utility::sanitize(handler->help_text_))); + } + response.add(AdminHtmlEnd); + return Http::Code::OK; +} + +} // namespace Server +} // namespace Envoy diff --git a/source/server/admin/admin_no_html.cc b/source/server/admin/admin_no_html.cc new file mode 100644 index 0000000000000..5d420809daf50 --- /dev/null +++ b/source/server/admin/admin_no_html.cc @@ -0,0 +1,15 @@ +#include "source/common/html/utility.h" +#include "source/server/admin/admin.h" + +namespace Envoy { +namespace Server { + +Http::Code AdminImpl::handlerAdminHome(absl::string_view, Http::ResponseHeaderMap& response_headers, + Buffer::Instance& response, AdminStream&) { + response_headers.setReferenceContentType(Http::Headers::get().ContentTypeValues.Text); + response.add("HTML output was disabled by building with --define=admin_html=disabled"); + return Http::Code::OK; +} + +} // namespace Server +} // namespace Envoy diff --git a/test/integration/integration_admin_test.cc b/test/integration/integration_admin_test.cc index e20c7c8b77c44..2db0dc6592fce 100644 --- a/test/integration/integration_admin_test.cc +++ b/test/integration/integration_admin_test.cc @@ -138,8 +138,13 @@ TEST_P(IntegrationAdminTest, Admin) { EXPECT_THAT(response->body(), HasSubstr("admin commands are:")); EXPECT_EQ("200", request("admin", "GET", "/", response)); +#ifdef ENVOY_ADMIN_HTML EXPECT_EQ("text/html; charset=UTF-8", contentType(response)); EXPECT_THAT(response->body(), HasSubstr("Envoy Admin")); +#else + EXPECT_EQ("text/plain", contentType(response)); + EXPECT_THAT(response->body(), HasSubstr("HTML output was disabled")); +#endif EXPECT_EQ("200", request("admin", "GET", "/server_info", response)); EXPECT_EQ("application/json", contentType(response)); diff --git a/test/server/admin/BUILD b/test/server/admin/BUILD index 559dafcd4c006..b8789d0bcc8fa 100644 --- a/test/server/admin/BUILD +++ b/test/server/admin/BUILD @@ -4,6 +4,7 @@ load( "envoy_cc_test", "envoy_cc_test_library", "envoy_package", + "envoy_select_admin_html", ) licenses(["notice"]) # Apache 2 @@ -57,6 +58,15 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "admin_html_test", + srcs = envoy_select_admin_html(["admin_html_test.cc"]), + deps = [ + ":admin_instance_lib", + "//source/server/admin:admin_lib", + ], +) + envoy_cc_test( name = "stats_handler_test", srcs = ["stats_handler_test.cc"], diff --git a/test/server/admin/admin_html_test.cc b/test/server/admin/admin_html_test.cc new file mode 100644 index 0000000000000..904407cc56e17 --- /dev/null +++ b/test/server/admin/admin_html_test.cc @@ -0,0 +1,45 @@ +#include "test/server/admin/admin_instance.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::HasSubstr; + +namespace Envoy { +namespace Server { + +INSTANTIATE_TEST_SUITE_P(IpVersions, AdminInstanceTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +TEST_P(AdminInstanceTest, EscapeHelpTextWithPunctuation) { + auto callback = [](absl::string_view, Http::HeaderMap&, Buffer::Instance&, + AdminStream&) -> Http::Code { return Http::Code::Accepted; }; + + // It's OK to have help text with HTML characters in it, but when we render the home + // page they need to be escaped. + const std::string planets = "jupiter>saturn>mars"; + EXPECT_TRUE(admin_.addHandler("/planets", planets, callback, true, false)); + + Http::TestResponseHeaderMapImpl header_map; + Buffer::OwnedImpl response; + EXPECT_EQ(Http::Code::OK, getCallback("/", header_map, response)); + const Http::HeaderString& content_type = header_map.ContentType()->value(); + EXPECT_THAT(std::string(content_type.getStringView()), HasSubstr("text/html")); + EXPECT_EQ(-1, response.search(planets.data(), planets.size(), 0, 0)); + const std::string escaped_planets = "jupiter>saturn>mars"; + EXPECT_NE(-1, response.search(escaped_planets.data(), escaped_planets.size(), 0, 0)); +} + +TEST_P(AdminInstanceTest, HelpUsesFormForMutations) { + Http::TestResponseHeaderMapImpl header_map; + Buffer::OwnedImpl response; + EXPECT_EQ(Http::Code::OK, getCallback("/", header_map, response)); + const std::string logging_action = "
Http::Code { return Http::Code::Accepted; }; - - // It's OK to have help text with HTML characters in it, but when we render the home - // page they need to be escaped. - const std::string planets = "jupiter>saturn>mars"; - EXPECT_TRUE(admin_.addHandler("/planets", planets, callback, true, false)); - - Http::TestResponseHeaderMapImpl header_map; - Buffer::OwnedImpl response; - EXPECT_EQ(Http::Code::OK, getCallback("/", header_map, response)); - const Http::HeaderString& content_type = header_map.ContentType()->value(); - EXPECT_THAT(std::string(content_type.getStringView()), HasSubstr("text/html")); - EXPECT_EQ(-1, response.search(planets.data(), planets.size(), 0, 0)); - const std::string escaped_planets = "jupiter>saturn>mars"; - EXPECT_NE(-1, response.search(escaped_planets.data(), escaped_planets.size(), 0, 0)); -} - -TEST_P(AdminInstanceTest, HelpUsesFormForMutations) { - Http::TestResponseHeaderMapImpl header_map; - Buffer::OwnedImpl response; - EXPECT_EQ(Http::Code::OK, getCallback("/", header_map, response)); - const std::string logging_action = "