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
5
  BuiltinResourceProvider() : histogram_js_(absl::StrCat(HistogramsJs1, HistogramsJs2)) {
58
5
    map_["admin_head_start.html"] = AdminHtmlStart;
59
5
    map_["admin.css"] = AdminCss;
60
5
    map_["active_stats.js"] = AdminActiveStatsJs;
61
5
    map_["histograms.js"] = histogram_js_;
62
5
    map_["active_params.html"] = AdminActiveParamsHtml;
63
5
  }
64

            
65
34
  absl::string_view getResource(absl::string_view resource_name, std::string&) override {
66
34
    return map_[resource_name];
67
34
  }
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
37
ProviderContainer& getProviderContainer() { MUTABLE_CONSTRUCT_ON_FIRST_USE(ProviderContainer); }
88

            
89
} // namespace
90

            
91
35
absl::string_view AdminHtmlUtil::getResource(absl::string_view resource_name, std::string& buf) {
92
35
  return getProviderContainer().provider_->getResource(resource_name, buf);
93
35
}
94

            
95
std::unique_ptr<AdminHtmlUtil::ResourceProvider>
96
2
AdminHtmlUtil::setResourceProvider(std::unique_ptr<ResourceProvider> resource_provider) {
97
2
  ProviderContainer& container = getProviderContainer();
98
2
  std::unique_ptr<ResourceProvider> prev = std::move(container.provider_);
99
2
  container.provider_ = std::move(resource_provider);
100
2
  return prev;
101
2
}
102

            
103
void AdminHtmlUtil::renderHead(Http::ResponseHeaderMap& response_headers,
104
14
                               Buffer::Instance& response) {
105
14
  response_headers.setReferenceContentType(Http::Headers::get().ContentTypeValues.Html);
106
14
  std::string buf1, buf2, buf3;
107
14
  response.addFragments({"<!DOCTYPE html>\n"
108
14
                         "<html lang='en'>\n"
109
14
                         "<head>\n",
110
14
                         absl::StrReplaceAll(getResource("admin_head_start.html", buf1),
111
14
                                             {{"@FAVICON@", EnvoyFavicon}}),
112
14
                         "<style>\n", getResource("admin.css", buf2), "</style>\n"});
113
14
  response.add("</head>\n<body>\n");
114
14
}
115

            
116
10
void AdminHtmlUtil::renderTableBegin(Buffer::Instance& response) {
117
10
  response.add(AdminHtmlTableBegin);
118
10
}
119

            
120
10
void AdminHtmlUtil::renderTableEnd(Buffer::Instance& response) { response.add(AdminHtmlTableEnd); }
121

            
122
12
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
143
                                       bool submit_on_change) {
130
143
  std::string value;
131
143
  if (query.has_value()) {
132
23
    auto result = query->getFirstValue(name);
133
23
    if (result.has_value()) {
134
7
      value = result.value();
135
7
    }
136
23
  }
137

            
138
143
  std::string on_change;
139
143
  if (submit_on_change) {
140
17
    on_change = absl::StrCat(" onchange='", path, ".submit()'");
141
17
  }
142

            
143
143
  switch (type) {
144
42
  case Admin::ParamDescriptor::Type::Boolean:
145
42
    response.addFragments({"<input type='checkbox' name='", name, "' id='", id, "' form='", path,
146
42
                           "'", on_change, value.empty() ? ">" : " checked/>"});
147
42
    break;
148
47
  case Admin::ParamDescriptor::Type::String: {
149
47
    std::string sanitized;
150
47
    if (!value.empty()) {
151
1
      sanitized = absl::StrCat(" value='", Html::Utility::sanitize(value), "'");
152
1
    }
153
47
    response.addFragments({"<input type='text' name='", name, "' id='", id, "' form='", path, "'",
154
47
                           on_change, sanitized, " />"});
155
47
    break;
156
  }
157
54
  case Admin::ParamDescriptor::Type::Enum:
158
54
    response.addFragments(
159
54
        {"\n    <select name='", name, "' id='", id, "' form='", path, "'", on_change, ">\n"});
160
194
    for (absl::string_view choice : enum_choices) {
161
194
      std::string sanitized_choice = Html::Utility::sanitize(choice);
162
194
      std::string sanitized_value = Html::Utility::sanitize(value);
163
194
      absl::string_view selected = (sanitized_value == sanitized_choice) ? " selected" : "";
164
194
      response.addFragments({"      <option value='", sanitized_choice, "'", selected, ">",
165
194
                             sanitized_choice, "</option>\n"});
166
194
    }
167
54
    response.add("    </select>\n  ");
168
54
    break;
169
143
  }
170
143
}
171

            
172
void AdminHtmlUtil::renderEndpointTableRow(Buffer::Instance& response,
173
                                           const Admin::UrlHandler& handler,
174
                                           OptRef<const Http::Utility::QueryParamsMulti> query,
175
168
                                           int index, bool submit_on_change, bool active) {
176
168
  absl::string_view path = handler.prefix_;
177

            
178
168
  if (path == "/") {
179
5
    return; // No need to print self-link to index page.
180
5
  }
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
163
  ASSERT(!path.empty());
193
163
  ASSERT(path[0] == '/');
194
163
  std::string sanitized_path = Html::Utility::sanitize(path.substr(1));
195
163
  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
163
  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
163
  const char* method = handler.mutates_server_state_ ? "post" : "get";
206
163
  if (submit_on_change) {
207
4
    response.addFragments({"\n<tr><td><form action='", path, "' method='", method, "' id='", path,
208
4
                           "' class='home-form'></form></td><td></td></tr>\n"});
209
162
  } else {
210
159
    response.addFragments({"\n<tr class='vert-space'><td></td><td></td></tr>\n<tr", row_class,
211
159
                           ">\n  <td class='home-data'>"});
212
159
    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
56
      response.addFragments({"<a href='", path, "'>", path, "</a>"});
217
103
    } else {
218
      // Render an explicit visible submit as a link (for GET) or button (for POST).
219
103
      const char* button_style = handler.mutates_server_state_ ? "" : " class='button-as-link'";
220
103
      response.addFragments({"<form action='", path, "' method='", method, "' id='", path,
221
103
                             "' class='home-form'>\n    <button", button_style, ">", path,
222
103
                             "</button>\n  </form>"});
223
103
    }
224
159
    response.addFragments({"</td>\n  <td class='home-data'>",
225
159
                           Html::Utility::sanitize(handler.help_text_), "</td>\n</tr>\n"});
226
159
  }
227

            
228
179
  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
143
    std::string id =
233
143
        absl::StrCat("param-", index, "-", absl::StrReplaceAll(path, {{"/", "-"}}), "-", param.id_);
234
143
    response.addFragments({"<tr", row_class, ">\n  <td class='option'>"});
235
143
    renderHandlerParam(response, id, param.id_, path, param.type_, query, param.enum_choices_,
236
143
                       submit_on_change && (!active || param.id_ == "format"));
237
143
    response.addFragments({"</td>\n  <td class='home-data'>", "<label for='", id, "'>",
238
143
                           Html::Utility::sanitize(param.help_), "</label></td>\n</tr>\n"});
239
143
  }
240
163
}
241

            
242
} // namespace Server
243
} // namespace Envoy