Line data Source code
1 : #include "source/server/admin/admin_html_util.h"
2 :
3 : #include "source/common/html/utility.h"
4 : #include "source/common/http/headers.h"
5 : #include "source/server/admin/html/admin_html_gen.h"
6 :
7 : #include "absl/strings/str_replace.h"
8 :
9 : namespace {
10 :
11 : const char AdminHtmlTableBegin[] = R"(
12 : <table class='home-table'>
13 : <thead>
14 : <tr>
15 : <th class='home-data'>Command</th>
16 : <th class='home-data'>Description</th>
17 : </tr>
18 : </thead>
19 : <tbody>
20 : )";
21 :
22 : const char AdminHtmlTableEnd[] = R"(
23 : </tbody>
24 : </table>
25 : )";
26 :
27 : /**
28 : * Favicon base64 image was harvested by screen-capturing the favicon from a Chrome tab
29 : * while visiting https://www.envoyproxy.io/. The resulting PNG was translated to base64
30 : * by dropping it into https://www.base64-image.de/ and then pasting the resulting string
31 : * below.
32 : *
33 : * The actual favicon source for that, https://www.envoyproxy.io/img/favicon.ico is nicer
34 : * because it's transparent, but is also 67646 bytes, which is annoying to inline. We could
35 : * just reference that rather than inlining it, but then the favicon won't work when visiting
36 : * the admin page from a network that can't see the internet.
37 : */
38 : const char EnvoyFavicon[] =
39 : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1"
40 : "BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAH9SURBVEhL7ZRdTttAFIUrUFaAX5w9gIhgUfzshFRK+gIbaVbA"
41 : "zwaqCly1dSpKk5A485/YCdXpHTB4BsdgVe0bD0cZ3Xsm38yZ8byTUuJ/6g3wqqoBrBhPTzmmLfptMbAzttJTpTKAF2MWC"
42 : "7ADCdNIwXZpvMMwayiIwwS874CcOc9VuQPR1dBBChPMITpFXXU45hukIIH6kHhzVqkEYB8F5HYGvZ5B7EvwmHt9K/59Cr"
43 : "U3QbY2RNYaQPYmJc+jPIBICNCcg20ZsAsCPfbcrFlRF+cJZpvXSJt9yMTxO/IAzJrCOfhJXiOgFEX/SbZmezTWxyNk4Q9"
44 : "anHMmjnzAhEyhAW8LCE6wl26J7ZFHH1FMYQxh567weQBOO1AW8D7P/UXAQySq/QvL8Fu9HfCEw4SKALm5BkC3bwjwhSKr"
45 : "A5hYAMXTJnPNiMyRBVzVjcgCyHiSm+8P+WGlnmwtP2RzbCMiQJ0d2KtmmmPorRHEhfMROVfTG5/fYrF5iWXzE80tfy9WP"
46 : "sCqx5Buj7FYH0LvDyHiqd+3otpsr4/fa5+xbEVQPfrYnntylQG5VGeMLBhgEfyE7o6e6qYzwHIjwl0QwXSvvTmrVAY4D5"
47 : "ddvT64wV0jRrr7FekO/XEjwuwwhuw7Ef7NY+dlfXpLb06EtHUJdVbsxvNUqBrwj/QGeEUSfwBAkmWHn5Bb/gAAAABJRU5";
48 :
49 : } // namespace
50 :
51 : namespace Envoy {
52 : namespace Server {
53 :
54 : namespace {
55 : class BuiltinResourceProvider : public AdminHtmlUtil::ResourceProvider {
56 : public:
57 0 : BuiltinResourceProvider() : histogram_js_(absl::StrCat(HistogramsJs1, HistogramsJs2)) {
58 0 : map_["admin_head_start.html"] = AdminHtmlStart;
59 0 : map_["admin.css"] = AdminCss;
60 0 : map_["active_stats.js"] = AdminActiveStatsJs;
61 0 : map_["histograms.js"] = histogram_js_;
62 0 : map_["active_params.html"] = AdminActiveParamsHtml;
63 0 : }
64 :
65 0 : absl::string_view getResource(absl::string_view resource_name, std::string&) override {
66 0 : return map_[resource_name];
67 0 : }
68 :
69 : private:
70 : const std::string histogram_js_;
71 : absl::flat_hash_map<absl::string_view, absl::string_view> map_;
72 : };
73 :
74 : // This is a hack to make a lazy-constructed holder for a pointer to the
75 : // resource provider.
76 : //
77 : // We use a mutable singleton rather than plumbing in the resource
78 : // provider. This is because the HTML features in the admin console can be
79 : // compiled out of Envoy. It's awkward to plumb the resource provider through
80 : // multiple layers that do not want to compile in the class definition. There
81 : // would be many `ifdefs` through constructors, initializers, etc.
82 : struct ProviderContainer {
83 : std::unique_ptr<AdminHtmlUtil::ResourceProvider> provider_{
84 : std::make_unique<BuiltinResourceProvider>()};
85 : };
86 :
87 0 : ProviderContainer& getProviderContainer() { MUTABLE_CONSTRUCT_ON_FIRST_USE(ProviderContainer); }
88 :
89 : } // namespace
90 :
91 0 : absl::string_view AdminHtmlUtil::getResource(absl::string_view resource_name, std::string& buf) {
92 0 : return getProviderContainer().provider_->getResource(resource_name, buf);
93 0 : }
94 :
95 : std::unique_ptr<AdminHtmlUtil::ResourceProvider>
96 0 : AdminHtmlUtil::setResourceProvider(std::unique_ptr<ResourceProvider> resource_provider) {
97 0 : ProviderContainer& container = getProviderContainer();
98 0 : std::unique_ptr<ResourceProvider> prev = std::move(container.provider_);
99 0 : container.provider_ = std::move(resource_provider);
100 0 : return prev;
101 0 : }
102 :
103 : void AdminHtmlUtil::renderHead(Http::ResponseHeaderMap& response_headers,
104 0 : Buffer::Instance& response) {
105 0 : response_headers.setReferenceContentType(Http::Headers::get().ContentTypeValues.Html);
106 0 : std::string buf1, buf2, buf3;
107 0 : response.addFragments({"<!DOCTYPE html>\n"
108 0 : "<html lang='en'>\n"
109 0 : "<head>\n",
110 0 : absl::StrReplaceAll(getResource("admin_head_start.html", buf1),
111 0 : {{"@FAVICON@", EnvoyFavicon}}),
112 0 : "<style>\n", getResource("admin.css", buf2), "</style>\n"});
113 0 : response.add("</head>\n<body>\n");
114 0 : }
115 :
116 0 : void AdminHtmlUtil::renderTableBegin(Buffer::Instance& response) {
117 0 : response.add(AdminHtmlTableBegin);
118 0 : }
119 :
120 0 : void AdminHtmlUtil::renderTableEnd(Buffer::Instance& response) { response.add(AdminHtmlTableEnd); }
121 :
122 0 : void AdminHtmlUtil::finalize(Buffer::Instance& response) { response.add("</body>\n</html>\n"); }
123 :
124 : void AdminHtmlUtil::renderHandlerParam(Buffer::Instance& response, absl::string_view id,
125 : absl::string_view name, absl::string_view path,
126 : Admin::ParamDescriptor::Type type,
127 : OptRef<const Http::Utility::QueryParamsMulti> query,
128 : const std::vector<absl::string_view>& enum_choices,
129 0 : bool submit_on_change) {
130 0 : std::string value;
131 0 : if (query.has_value()) {
132 0 : auto result = query->getFirstValue(name);
133 0 : if (result.has_value()) {
134 0 : value = result.value();
135 0 : }
136 0 : }
137 :
138 0 : std::string on_change;
139 0 : if (submit_on_change) {
140 0 : on_change = absl::StrCat(" onchange='", path, ".submit()'");
141 0 : }
142 :
143 0 : switch (type) {
144 0 : case Admin::ParamDescriptor::Type::Boolean:
145 0 : response.addFragments({"<input type='checkbox' name='", name, "' id='", id, "' form='", path,
146 0 : "'", on_change, value.empty() ? ">" : " checked/>"});
147 0 : break;
148 0 : case Admin::ParamDescriptor::Type::String: {
149 0 : std::string sanitized;
150 0 : if (!value.empty()) {
151 0 : sanitized = absl::StrCat(" value='", Html::Utility::sanitize(value), "'");
152 0 : }
153 0 : response.addFragments({"<input type='text' name='", name, "' id='", id, "' form='", path, "'",
154 0 : on_change, sanitized, " />"});
155 0 : break;
156 0 : }
157 0 : case Admin::ParamDescriptor::Type::Enum:
158 0 : response.addFragments(
159 0 : {"\n <select name='", name, "' id='", id, "' form='", path, "'", on_change, ">\n"});
160 0 : for (absl::string_view choice : enum_choices) {
161 0 : std::string sanitized_choice = Html::Utility::sanitize(choice);
162 0 : std::string sanitized_value = Html::Utility::sanitize(value);
163 0 : absl::string_view selected = (sanitized_value == sanitized_choice) ? " selected" : "";
164 0 : response.addFragments({" <option value='", sanitized_choice, "'", selected, ">",
165 0 : sanitized_choice, "</option>\n"});
166 0 : }
167 0 : response.add(" </select>\n ");
168 0 : break;
169 0 : }
170 0 : }
171 :
172 : void AdminHtmlUtil::renderEndpointTableRow(Buffer::Instance& response,
173 : const Admin::UrlHandler& handler,
174 : OptRef<const Http::Utility::QueryParamsMulti> query,
175 0 : int index, bool submit_on_change, bool active) {
176 0 : absl::string_view path = handler.prefix_;
177 :
178 0 : if (path == "/") {
179 0 : return; // No need to print self-link to index page.
180 0 : }
181 :
182 : // Remove the leading slash from the link, so that the admin page can be
183 : // rendered as part of another console, on a sub-path.
184 : //
185 : // E.g. consider a downstream dashboard that embeds the Envoy admin console.
186 : // In that case, the "/stats" endpoint would be at
187 : // https://DASHBOARD/envoy_admin/stats. If the links we present on the home
188 : // page are absolute (e.g. "/stats") they won't work in the context of the
189 : // dashboard. Removing the leading slash, they will work properly in both
190 : // the raw admin console and when embedded in another page and URL
191 : // hierarchy.
192 0 : ASSERT(!path.empty());
193 0 : ASSERT(path[0] == '/');
194 0 : std::string sanitized_path = Html::Utility::sanitize(path.substr(1));
195 0 : path = sanitized_path;
196 :
197 : // Alternate gray and white param-blocks. The pure CSS way of coloring based
198 : // on row index doesn't work correctly for us as we are using a row for each
199 : // parameter, and we want each endpoint/option-block to be colored the same.
200 0 : const char* row_class = (index & 1) ? " class='gray'" : "";
201 :
202 : // For handlers that mutate state, render the link as a button in a POST form,
203 : // rather than an anchor tag. This should discourage crawlers that find the /
204 : // page from accidentally mutating all the server state by GETting all the hrefs.
205 0 : const char* method = handler.mutates_server_state_ ? "post" : "get";
206 0 : if (submit_on_change) {
207 0 : response.addFragments({"\n<tr><td><form action='", path, "' method='", method, "' id='", path,
208 0 : "' class='home-form'></form></td><td></td></tr>\n"});
209 0 : } else {
210 0 : response.addFragments({"\n<tr class='vert-space'><td></td><td></td></tr>\n<tr", row_class,
211 0 : ">\n <td class='home-data'>"});
212 0 : if (!handler.mutates_server_state_ && handler.params_.empty()) {
213 : // GET requests without parameters can be simple links rather than forms with
214 : // buttons that are rendered as links. This simplification improves the
215 : // usability of the page with screen-readers.
216 0 : response.addFragments({"<a href='", path, "'>", path, "</a>"});
217 0 : } else {
218 : // Render an explicit visible submit as a link (for GET) or button (for POST).
219 0 : const char* button_style = handler.mutates_server_state_ ? "" : " class='button-as-link'";
220 0 : response.addFragments({"<form action='", path, "' method='", method, "' id='", path,
221 0 : "' class='home-form'>\n <button", button_style, ">", path,
222 0 : "</button>\n </form>"});
223 0 : }
224 0 : response.addFragments({"</td>\n <td class='home-data'>",
225 0 : Html::Utility::sanitize(handler.help_text_), "</td>\n</tr>\n"});
226 0 : }
227 :
228 0 : for (const Admin::ParamDescriptor& param : handler.params_) {
229 : // Give each parameter a unique number. Note that this naming is also referenced in
230 : // active_stats.js which looks directly at the parameter widgets to find the
231 : // current values during JavaScript-driven active updates.
232 0 : std::string id =
233 0 : absl::StrCat("param-", index, "-", absl::StrReplaceAll(path, {{"/", "-"}}), "-", param.id_);
234 0 : response.addFragments({"<tr", row_class, ">\n <td class='option'>"});
235 0 : renderHandlerParam(response, id, param.id_, path, param.type_, query, param.enum_choices_,
236 0 : submit_on_change && (!active || param.id_ == "format"));
237 0 : response.addFragments({"</td>\n <td class='home-data'>", "<label for='", id, "'>",
238 0 : Html::Utility::sanitize(param.help_), "</label></td>\n</tr>\n"});
239 0 : }
240 0 : }
241 :
242 : } // namespace Server
243 : } // namespace Envoy
|