/Users/deen/code/yugabyte-db/src/yb/server/webserver.cc
Line | Count | Source (jump to first uncovered line) |
1 | | // Licensed to the Apache Software Foundation (ASF) under one |
2 | | // or more contributor license agreements. See the NOTICE file |
3 | | // distributed with this work for additional information |
4 | | // regarding copyright ownership. The ASF licenses this file |
5 | | // to you under the Apache License, Version 2.0 (the |
6 | | // "License"); you may not use this file except in compliance |
7 | | // with the License. You may obtain a copy of the License at |
8 | | // |
9 | | // http://www.apache.org/licenses/LICENSE-2.0 |
10 | | // |
11 | | // Unless required by applicable law or agreed to in writing, |
12 | | // software distributed under the License is distributed on an |
13 | | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
14 | | // KIND, either express or implied. See the License for the |
15 | | // specific language governing permissions and limitations |
16 | | // under the License. |
17 | | // |
18 | | // The following only applies to changes made to this file as part of YugaByte development. |
19 | | // |
20 | | // Portions Copyright (c) YugaByte, Inc. |
21 | | // |
22 | | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
23 | | // in compliance with the License. You may obtain a copy of the License at |
24 | | // |
25 | | // http://www.apache.org/licenses/LICENSE-2.0 |
26 | | // |
27 | | // Unless required by applicable law or agreed to in writing, software distributed under the License |
28 | | // is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
29 | | // or implied. See the License for the specific language governing permissions and limitations |
30 | | // under the License. |
31 | | // |
32 | | // Licensed under the Apache License, Version 2.0 (the "License"); |
33 | | // you may not use this file except in compliance with the License. |
34 | | // You may obtain a copy of the License at |
35 | | // |
36 | | // http://www.apache.org/licenses/LICENSE-2.0 |
37 | | // |
38 | | // Unless required by applicable law or agreed to in writing, software |
39 | | // distributed under the License is distributed on an "AS IS" BASIS, |
40 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
41 | | // See the License for the specific language governing permissions and |
42 | | // limitations under the License. |
43 | | |
44 | | #include "yb/server/webserver.h" |
45 | | |
46 | | #include <stdio.h> |
47 | | |
48 | | #include <algorithm> |
49 | | #include <functional> |
50 | | #include <map> |
51 | | #include <string> |
52 | | #include <type_traits> |
53 | | #include <vector> |
54 | | |
55 | | #include <boost/algorithm/string.hpp> |
56 | | #include <cds/init.h> |
57 | | #include <glog/logging.h> |
58 | | #include <squeasel.h> |
59 | | |
60 | | #include "yb/gutil/dynamic_annotations.h" |
61 | | #include "yb/gutil/map-util.h" |
62 | | #include "yb/gutil/stl_util.h" |
63 | | #include "yb/gutil/stringprintf.h" |
64 | | #include "yb/gutil/strings/join.h" |
65 | | #include "yb/gutil/strings/numbers.h" |
66 | | #include "yb/gutil/strings/split.h" |
67 | | #include "yb/gutil/strings/stringpiece.h" |
68 | | #include "yb/gutil/strings/strip.h" |
69 | | |
70 | | #include "yb/util/env.h" |
71 | | #include "yb/util/flag_tags.h" |
72 | | #include "yb/util/net/net_util.h" |
73 | | #include "yb/util/net/sockaddr.h" |
74 | | #include "yb/util/scope_exit.h" |
75 | | #include "yb/util/shared_lock.h" |
76 | | #include "yb/util/status.h" |
77 | | #include "yb/util/status_format.h" |
78 | | #include "yb/util/url-coding.h" |
79 | | #include "yb/util/zlib.h" |
80 | | |
81 | | #if defined(__APPLE__) |
82 | | typedef sig_t sighandler_t; |
83 | | #endif |
84 | | |
85 | | DEFINE_int32(webserver_max_post_length_bytes, 1024 * 1024, |
86 | | "The maximum length of a POST request that will be accepted by " |
87 | | "the embedded web server."); |
88 | | TAG_FLAG(webserver_max_post_length_bytes, advanced); |
89 | | TAG_FLAG(webserver_max_post_length_bytes, runtime); |
90 | | |
91 | | DEFINE_int32(webserver_zlib_compression_level, 1, |
92 | | "The zlib compression level." |
93 | | "Lower compression levels result in faster execution, but less compression"); |
94 | | TAG_FLAG(webserver_zlib_compression_level, advanced); |
95 | | TAG_FLAG(webserver_zlib_compression_level, runtime); |
96 | | |
97 | | DEFINE_uint64(webserver_compression_threshold_kb, 4, |
98 | | "The threshold of response size above which compression is performed." |
99 | | "Default value is 4KB"); |
100 | | TAG_FLAG(webserver_compression_threshold_kb, advanced); |
101 | | TAG_FLAG(webserver_compression_threshold_kb, runtime); |
102 | | |
103 | | namespace yb { |
104 | | |
105 | | using std::string; |
106 | | using std::stringstream; |
107 | | using std::vector; |
108 | | using std::make_pair; |
109 | | |
110 | | using namespace std::placeholders; |
111 | | |
112 | | Webserver::Webserver(const WebserverOptions& opts, const std::string& server_name) |
113 | | : opts_(opts), |
114 | | context_(nullptr), |
115 | 18.4k | server_name_(server_name) { |
116 | 18.4k | string host = opts.bind_interface.empty() ? "0.0.0.0" : opts.bind_interface; |
117 | 18.4k | http_address_ = host + ":" + std::to_string(opts.port); |
118 | 18.4k | } |
119 | | |
120 | 212 | Webserver::~Webserver() { |
121 | 212 | Stop(); |
122 | 212 | STLDeleteValues(&path_handlers_); |
123 | 212 | } |
124 | | |
125 | 6 | void Webserver::RootHandler(const Webserver::WebRequest& args, Webserver::WebResponse* resp) { |
126 | 6 | } |
127 | | |
128 | 236 | void Webserver::BuildArgumentMap(const string& args, ArgumentMap* output) { |
129 | 236 | vector<GStringPiece> arg_pairs = strings::Split(args, "&"); |
130 | | |
131 | 236 | for (const GStringPiece& arg_pair : arg_pairs) { |
132 | 236 | vector<GStringPiece> key_value = strings::Split(arg_pair, "="); |
133 | 236 | if (key_value.empty()) continue; |
134 | | |
135 | 236 | string key; |
136 | 236 | if (!UrlDecode(key_value[0].ToString(), &key)) continue; |
137 | 236 | string value; |
138 | 236 | if (!UrlDecode((key_value.size() >= 2 ? key_value[1].ToString() : ""), &value)) continue; |
139 | 236 | boost::to_lower(key); |
140 | 236 | (*output)[key] = value; |
141 | 236 | } |
142 | 236 | } |
143 | | |
144 | 36.5k | bool Webserver::IsSecure() const { |
145 | 36.5k | return !opts_.certificate_file.empty(); |
146 | 36.5k | } |
147 | | |
148 | 18.0k | Status Webserver::BuildListenSpec(string* spec) const { |
149 | 18.0k | std::vector<Endpoint> endpoints; |
150 | 18.0k | RETURN_NOT_OK(ParseAddressList(http_address_, 80, &endpoints)); |
151 | 18.0k | if (endpoints.empty()) { |
152 | 0 | return STATUS_FORMAT( |
153 | 0 | ConfigurationError, |
154 | 0 | "No IPs available for address $0", http_address_); |
155 | 0 | } |
156 | 18.0k | std::vector<string> parts; |
157 | 18.4k | for (const auto& endpoint : endpoints) { |
158 | | // Mongoose makes sockets with 's' suffixes accept SSL traffic only |
159 | 18.4k | parts.push_back(ToString(endpoint) + (IsSecure() ? "s" : "")); |
160 | 18.4k | } |
161 | | |
162 | 18.0k | JoinStrings(parts, ",", spec); |
163 | 18.0k | LOG(INFO) << "Webserver listen spec is " << *spec; |
164 | 18.0k | return Status::OK(); |
165 | 18.0k | } |
166 | | |
167 | 18.0k | Status Webserver::Start() { |
168 | 18.0k | LOG(INFO) << "Starting webserver on " << http_address_; |
169 | | |
170 | 18.0k | vector<const char*> options; |
171 | | |
172 | 18.0k | if (static_pages_available()) { |
173 | 18.0k | LOG(INFO) << "Document root: " << opts_.doc_root; |
174 | 18.0k | options.push_back("document_root"); |
175 | 18.0k | options.push_back(opts_.doc_root.c_str()); |
176 | 18.0k | options.push_back("enable_directory_listing"); |
177 | 18.0k | options.push_back("no"); |
178 | 0 | } else { |
179 | 0 | LOG(INFO)<< "Document root disabled"; |
180 | 0 | } |
181 | | |
182 | 18.0k | if (IsSecure()) { |
183 | 0 | LOG(INFO) << "Webserver: Enabling HTTPS support"; |
184 | 0 | options.push_back("ssl_certificate"); |
185 | 0 | options.push_back(opts_.certificate_file.c_str()); |
186 | 0 | } |
187 | | |
188 | 18.0k | if (!opts_.authentication_domain.empty()) { |
189 | 0 | options.push_back("authentication_domain"); |
190 | 0 | options.push_back(opts_.authentication_domain.c_str()); |
191 | 0 | } |
192 | | |
193 | 18.0k | if (!opts_.password_file.empty()) { |
194 | | // Mongoose doesn't log anything if it can't stat the password file (but will if it |
195 | | // can't open it, which it tries to do during a request) |
196 | 0 | if (!Env::Default()->FileExists(opts_.password_file)) { |
197 | 0 | stringstream ss; |
198 | 0 | ss << "Webserver: Password file does not exist: " << opts_.password_file; |
199 | 0 | return STATUS(InvalidArgument, ss.str()); |
200 | 0 | } |
201 | 0 | LOG(INFO) << "Webserver: Password file is " << opts_.password_file; |
202 | 0 | options.push_back("global_passwords_file"); |
203 | 0 | options.push_back(opts_.password_file.c_str()); |
204 | 0 | } |
205 | | |
206 | 18.0k | options.push_back("listening_ports"); |
207 | 18.0k | string listening_str; |
208 | 18.0k | RETURN_NOT_OK(BuildListenSpec(&listening_str)); |
209 | 18.0k | options.push_back(listening_str.c_str()); |
210 | | |
211 | | // Num threads |
212 | 18.0k | options.push_back("num_threads"); |
213 | 18.0k | string num_threads_str = SimpleItoa(opts_.num_worker_threads); |
214 | 18.0k | options.push_back(num_threads_str.c_str()); |
215 | | |
216 | | // Options must be a NULL-terminated list |
217 | 18.0k | options.push_back(nullptr); |
218 | | |
219 | | // mongoose ignores SIGCHLD and we need it to run kinit. This means that since |
220 | | // mongoose does not reap its own children CGI programs must be avoided. |
221 | | // Save the signal handler so we can restore it after mongoose sets it to be ignored. |
222 | 18.0k | sighandler_t sig_chld = signal(SIGCHLD, SIG_DFL); |
223 | | |
224 | 18.0k | sq_callbacks callbacks; |
225 | 18.0k | memset(&callbacks, 0, sizeof(callbacks)); |
226 | 18.0k | callbacks.begin_request = &Webserver::BeginRequestCallbackStatic; |
227 | 18.0k | callbacks.log_message = &Webserver::LogMessageCallbackStatic; |
228 | 18.0k | callbacks.enter_worker_thread = &Webserver::EnterWorkerThreadCallbackStatic; |
229 | 18.0k | callbacks.leave_worker_thread = &Webserver::LeaveWorkerThreadCallbackStatic; |
230 | | |
231 | | // To work around not being able to pass member functions as C callbacks, we store a |
232 | | // pointer to this server in the per-server state, and register a static method as the |
233 | | // default callback. That method unpacks the pointer to this and calls the real |
234 | | // callback. |
235 | 18.0k | context_ = sq_start(&callbacks, reinterpret_cast<void*>(this), &options[0]); |
236 | | |
237 | | // Restore the child signal handler so wait() works properly. |
238 | 18.0k | signal(SIGCHLD, sig_chld); |
239 | | |
240 | 18.0k | if (context_ == nullptr) { |
241 | 3 | stringstream error_msg; |
242 | 3 | error_msg << "Webserver: Could not start on address " << http_address_; |
243 | 3 | TryRunLsof(Endpoint(IpAddress(), opts_.port)); |
244 | 3 | return STATUS(NetworkError, error_msg.str()); |
245 | 3 | } |
246 | | |
247 | 18.0k | PathHandlerCallback default_callback = |
248 | 18.0k | std::bind(boost::mem_fn(&Webserver::RootHandler), this, _1, _2); |
249 | | |
250 | 18.0k | RegisterPathHandler("/", "Home", default_callback); |
251 | | |
252 | 18.0k | std::vector<Endpoint> addrs; |
253 | 18.0k | RETURN_NOT_OK(GetBoundAddresses(&addrs)); |
254 | 18.0k | string bound_addresses_str; |
255 | 18.4k | for (const auto& addr : addrs) { |
256 | 18.4k | if (!bound_addresses_str.empty()) { |
257 | 336 | bound_addresses_str += ", "; |
258 | 336 | } |
259 | 18.4k | bound_addresses_str += "http://" + ToString(addr) + "/"; |
260 | 18.4k | } |
261 | | |
262 | 18.0k | LOG(INFO) << "Webserver started. Bound to: " << bound_addresses_str; |
263 | 18.0k | return Status::OK(); |
264 | 18.0k | } |
265 | | |
266 | 666 | void Webserver::Stop() { |
267 | 666 | std::lock_guard<std::mutex> lock_(stop_mutex_); |
268 | 666 | if (context_ != nullptr) { |
269 | 166 | sq_stop(context_); |
270 | 166 | context_ = nullptr; |
271 | 166 | } |
272 | 666 | } |
273 | | |
274 | 11.3k | Status Webserver::GetInputHostPort(HostPort* hp) const { |
275 | 11.3k | std::vector<HostPort> parsed_hps; |
276 | 11.3k | RETURN_NOT_OK(HostPort::ParseStrings( |
277 | 11.3k | http_address_, |
278 | 11.3k | 0 /* default port */, |
279 | 11.3k | &parsed_hps)); |
280 | | |
281 | | // Webserver always gets a single host:port specification from WebserverOptions. |
282 | 11.3k | DCHECK_EQ(parsed_hps.size(), 1); |
283 | 11.3k | if (parsed_hps.size() != 1) { |
284 | 0 | return STATUS(InvalidArgument, "Expected single host port in WebserverOptions host port"); |
285 | 0 | } |
286 | | |
287 | 11.3k | *hp = parsed_hps[0]; |
288 | 11.3k | return Status::OK(); |
289 | 11.3k | } |
290 | | |
291 | 20.7k | Status Webserver::GetBoundAddresses(std::vector<Endpoint>* addrs_ptr) const { |
292 | 20.7k | if (!context_) { |
293 | 2 | return STATUS(IllegalState, "Not started"); |
294 | 2 | } |
295 | | |
296 | 20.7k | struct sockaddr_storage** sockaddrs = nullptr; |
297 | 20.7k | int num_addrs; |
298 | | |
299 | 20.7k | if (sq_get_bound_addresses(context_, &sockaddrs, &num_addrs)) { |
300 | 0 | return STATUS(NetworkError, "Unable to get bound addresses from Mongoose"); |
301 | 0 | } |
302 | 20.7k | auto cleanup = ScopeExit([sockaddrs, num_addrs] { |
303 | 20.7k | if (!sockaddrs) { |
304 | 0 | return; |
305 | 0 | } |
306 | 42.1k | for (int i = 0; i < num_addrs; ++i) { |
307 | 21.3k | free(sockaddrs[i]); |
308 | 21.3k | } |
309 | 20.7k | free(sockaddrs); |
310 | 20.7k | }); |
311 | 20.7k | auto& addrs = *addrs_ptr; |
312 | 20.7k | addrs.resize(num_addrs); |
313 | | |
314 | 42.1k | for (int i = 0; i < num_addrs; i++) { |
315 | 21.3k | switch (sockaddrs[i]->ss_family) { |
316 | 20.7k | case AF_INET: { |
317 | 20.7k | sockaddr_in* addr = reinterpret_cast<struct sockaddr_in*>(sockaddrs[i]); |
318 | 20.7k | RSTATUS_DCHECK( |
319 | 20.7k | addrs[i].capacity() >= sizeof(*addr), IllegalState, "Unexpected size of struct"); |
320 | 20.7k | memcpy(addrs[i].data(), addr, sizeof(*addr)); |
321 | 20.7k | break; |
322 | 0 | } |
323 | 672 | case AF_INET6: { |
324 | 672 | sockaddr_in6* addr6 = reinterpret_cast<struct sockaddr_in6*>(sockaddrs[i]); |
325 | 672 | RSTATUS_DCHECK( |
326 | 672 | addrs[i].capacity() >= sizeof(*addr6), IllegalState, "Unexpected size of struct"); |
327 | 672 | memcpy(addrs[i].data(), addr6, sizeof(*addr6)); |
328 | 672 | break; |
329 | 0 | } |
330 | 0 | default: { |
331 | 0 | LOG(ERROR) << "Unexpected address family: " << sockaddrs[i]->ss_family; |
332 | 0 | RSTATUS_DCHECK(false, IllegalState, "Unexpected address family"); |
333 | 0 | break; |
334 | 0 | } |
335 | 21.3k | } |
336 | 21.3k | } |
337 | | |
338 | 20.7k | return Status::OK(); |
339 | 20.7k | } |
340 | | int Webserver::LogMessageCallbackStatic(const struct sq_connection* connection, |
341 | 4 | const char* message) { |
342 | 4 | if (message != nullptr) { |
343 | 4 | LOG(INFO) << "Webserver: " << message; |
344 | 4 | return 1; |
345 | 4 | } |
346 | 0 | return 0; |
347 | 0 | } |
348 | | |
349 | 15.6k | int Webserver::BeginRequestCallbackStatic(struct sq_connection* connection) { |
350 | 15.6k | struct sq_request_info* request_info = sq_get_request_info(connection); |
351 | 15.6k | Webserver* instance = reinterpret_cast<Webserver*>(request_info->user_data); |
352 | 15.6k | return instance->BeginRequestCallback(connection, request_info); |
353 | 15.6k | } |
354 | | |
355 | | int Webserver::BeginRequestCallback(struct sq_connection* connection, |
356 | 15.6k | struct sq_request_info* request_info) { |
357 | 15.6k | PathHandler* handler; |
358 | 15.6k | { |
359 | 15.6k | SharedLock<std::shared_timed_mutex> lock(lock_); |
360 | 15.6k | PathHandlerMap::const_iterator it = path_handlers_.find(request_info->uri); |
361 | 15.6k | if (it == path_handlers_.end()) { |
362 | | // Let Mongoose deal with this request; returning NULL will fall through |
363 | | // to the default handler which will serve files. |
364 | 3 | if (static_pages_available()) { |
365 | 0 | VLOG(2) << "HTTP File access: " << request_info->uri; |
366 | 3 | return 0; |
367 | 0 | } else { |
368 | 0 | sq_printf(connection, "HTTP/1.1 404 Not Found\r\n" |
369 | 0 | "Content-Type: text/plain\r\n\r\n"); |
370 | 0 | sq_printf(connection, "No handler for URI %s\r\n\r\n", request_info->uri); |
371 | 0 | return 1; |
372 | 0 | } |
373 | 15.6k | } |
374 | 15.6k | handler = it->second; |
375 | 15.6k | } |
376 | | |
377 | 15.6k | return RunPathHandler(*handler, connection, request_info); |
378 | 15.6k | } |
379 | | |
380 | | int Webserver::RunPathHandler(const PathHandler& handler, |
381 | | struct sq_connection* connection, |
382 | 15.6k | struct sq_request_info* request_info) { |
383 | | // Should we render with css styles? |
384 | 15.6k | bool use_style = true; |
385 | | |
386 | 15.6k | WebRequest req; |
387 | 15.6k | req.redirect_uri = request_info->uri; |
388 | 15.6k | if (request_info->query_string != nullptr) { |
389 | 236 | req.query_string = request_info->query_string; |
390 | 236 | BuildArgumentMap(request_info->query_string, &req.parsed_args); |
391 | 236 | } |
392 | 15.6k | req.request_method = request_info->request_method; |
393 | 15.6k | if (req.request_method == "POST") { |
394 | 6 | const char* content_len_str = sq_get_header(connection, "Content-Length"); |
395 | 6 | int32_t content_len = 0; |
396 | 6 | if (content_len_str == nullptr || |
397 | 6 | !safe_strto32(content_len_str, &content_len)) { |
398 | 0 | sq_printf(connection, "HTTP/1.1 411 Length Required\r\n"); |
399 | 0 | return 1; |
400 | 0 | } |
401 | 6 | if (content_len > FLAGS_webserver_max_post_length_bytes) { |
402 | | // TODO: for this and other HTTP requests, we should log the |
403 | | // remote IP, etc. |
404 | 1 | LOG(WARNING) << "Rejected POST with content length " << content_len; |
405 | 1 | sq_printf(connection, "HTTP/1.1 413 Request Entity Too Large\r\n"); |
406 | 1 | return 1; |
407 | 1 | } |
408 | | |
409 | 5 | char buf[8192]; |
410 | 5 | int rem = content_len; |
411 | 39 | while (rem > 0) { |
412 | 34 | int n = sq_read(connection, buf, std::min<int>(sizeof(buf), rem)); |
413 | 34 | if (n <= 0) { |
414 | 0 | LOG(WARNING) << "error reading POST data: expected " |
415 | 0 | << content_len << " bytes but only read " |
416 | 0 | << req.post_data.size(); |
417 | 0 | sq_printf(connection, "HTTP/1.1 500 Internal Server Error\r\n"); |
418 | 0 | return 1; |
419 | 0 | } |
420 | | |
421 | 34 | req.post_data.append(buf, n); |
422 | 34 | rem -= n; |
423 | 34 | } |
424 | 5 | } |
425 | | |
426 | 15.6k | if (!handler.is_styled() || ContainsKey(req.parsed_args, "raw")) { |
427 | 15.5k | use_style = false; |
428 | 15.5k | } |
429 | | |
430 | 15.6k | WebResponse resp; |
431 | 15.6k | WebResponse* resp_ptr = &resp; |
432 | | // Default response code should be OK. |
433 | 15.6k | resp_ptr->code = 200; |
434 | 15.6k | stringstream *output = &resp_ptr->output; |
435 | 15.6k | if (use_style) { |
436 | 69 | BootstrapPageHeader(output); |
437 | 69 | } |
438 | 15.6k | for (const PathHandlerCallback& callback_ : handler.callbacks()) { |
439 | 15.6k | callback_(req, resp_ptr); |
440 | 15.6k | if (resp_ptr->code == 503) { |
441 | 2 | sq_printf(connection, "HTTP/1.1 503 Service Unavailable\r\n"); |
442 | 2 | return 1; |
443 | 2 | } |
444 | 15.6k | } |
445 | 15.6k | if (use_style) { |
446 | 69 | BootstrapPageFooter(output); |
447 | 69 | } |
448 | | // Check if gzip compression is accepted by the caller. If so, compress the |
449 | | // content and replace the prerendered output. |
450 | 15.6k | const char* accept_encoding_str = sq_get_header(connection, "Accept-Encoding"); |
451 | 15.6k | bool is_compressed = false; |
452 | 15.6k | vector<string> encodings = strings::Split(accept_encoding_str, ","); |
453 | 9 | for (string& encoding : encodings) { |
454 | 9 | StripWhiteSpace(&encoding); |
455 | 9 | if (encoding == "gzip") { |
456 | | // Don't bother compressing empty content. |
457 | 2 | const string& uncompressed = resp_ptr->output.str(); |
458 | 2 | if (uncompressed.size() < FLAGS_webserver_compression_threshold_kb * 1024) { |
459 | 0 | break; |
460 | 0 | } |
461 | | |
462 | 2 | std::ostringstream oss; |
463 | 2 | int level = FLAGS_webserver_zlib_compression_level > 0 && |
464 | 2 | FLAGS_webserver_zlib_compression_level <= 9 ? |
465 | 2 | FLAGS_webserver_zlib_compression_level : 1; |
466 | 2 | Status s = zlib::CompressLevel(uncompressed, level, &oss); |
467 | 2 | if (s.ok()) { |
468 | 2 | resp_ptr->output.str(oss.str()); |
469 | 2 | is_compressed = true; |
470 | 0 | } else { |
471 | 0 | LOG(WARNING) << "Could not compress output: " << s.ToString(); |
472 | 0 | } |
473 | 2 | break; |
474 | 2 | } |
475 | 9 | } |
476 | | |
477 | 15.6k | string str = output->str(); |
478 | | // Without styling, render the page as plain text |
479 | 15.6k | if (!use_style) { |
480 | 15.5k | sq_printf(connection, "HTTP/1.1 200 OK\r\n" |
481 | 15.5k | "Content-Type: text/plain\r\n" |
482 | 15.5k | "Content-Length: %zd\r\n" |
483 | 15.5k | "%s" |
484 | 15.5k | "Access-Control-Allow-Origin: *\r\n" |
485 | 15.5k | "\r\n", str.length(), is_compressed ? "Content-Encoding: gzip\r\n" : ""); |
486 | 69 | } else { |
487 | 69 | sq_printf(connection, "HTTP/1.1 200 OK\r\n" |
488 | 69 | "Content-Type: text/html\r\n" |
489 | 69 | "Content-Length: %zd\r\n" |
490 | 69 | "%s" |
491 | 69 | "Access-Control-Allow-Origin: *\r\n" |
492 | 67 | "\r\n", str.length(), is_compressed ? "Content-Encoding: gzip\r\n" : ""); |
493 | 69 | } |
494 | | |
495 | | // Make sure to use sq_write for printing the body; sq_printf truncates at 8kb |
496 | 15.6k | sq_write(connection, str.c_str(), str.length()); |
497 | 15.6k | return 1; |
498 | 15.6k | } |
499 | | |
500 | | void Webserver::RegisterPathHandler(const string& path, |
501 | | const string& alias, |
502 | | const PathHandlerCallback& callback, |
503 | | bool is_styled, |
504 | | bool is_on_nav_bar, |
505 | 677k | const std::string icon) { |
506 | 677k | std::lock_guard<std::shared_timed_mutex> lock(lock_); |
507 | 677k | auto it = path_handlers_.find(path); |
508 | 677k | if (it == path_handlers_.end()) { |
509 | 666k | it = path_handlers_.insert( |
510 | 666k | make_pair(path, new PathHandler(is_styled, is_on_nav_bar, alias, icon))).first; |
511 | 666k | } |
512 | 677k | it->second->AddCallback(callback); |
513 | 677k | } |
514 | | |
515 | | const char* const PAGE_HEADER = "<!DOCTYPE html>" |
516 | | "<html>" |
517 | | " <head>" |
518 | | " <title>YugabyteDB</title>" |
519 | | " <link rel='shortcut icon' href='/favicon.ico'>" |
520 | | " <link href='/bootstrap/css/bootstrap.min.css' rel='stylesheet' media='screen' />" |
521 | | " <link href='/bootstrap/css/bootstrap-theme.min.css' rel='stylesheet' media='screen' />" |
522 | | " <link href='/font-awesome/css/font-awesome.min.css' rel='stylesheet' media='screen' />" |
523 | | " <link href='/yb.css' rel='stylesheet' media='screen' />" |
524 | | " </head>" |
525 | | "\n" |
526 | | "<body>" |
527 | | "\n"; |
528 | | |
529 | | static const char* const NAVIGATION_BAR_PREFIX = |
530 | | " <nav class=\"navbar navbar-fixed-top navbar-inverse sidebar-wrapper\" role=\"navigation\">" |
531 | | " <ul class=\"nav sidebar-nav\">" |
532 | | " <li><a href='/'><img src='/logo.png' alt='YugabyteDB' class='nav-logo' /></a></li>" |
533 | | "\n"; |
534 | | |
535 | | static const char* const NAVIGATION_BAR_SUFFIX = |
536 | | " </ul>" |
537 | | " </nav>" |
538 | | "\n\n" |
539 | | " <div class='yb-main container-fluid'>"; |
540 | | |
541 | 69 | void Webserver::BootstrapPageHeader(stringstream* output) { |
542 | 69 | (*output) << PAGE_HEADER; |
543 | 69 | (*output) << NAVIGATION_BAR_PREFIX; |
544 | 2.52k | for (const PathHandlerMap::value_type& handler : path_handlers_) { |
545 | 2.52k | if (handler.second->is_on_nav_bar()) { |
546 | 253 | (*output) << "<li class='nav-item'>" |
547 | 253 | << "<a href='" << handler.first << "'>" |
548 | 253 | << "<div><i class='" << handler.second->icon() << "'aria-hidden='true'></i></div>" |
549 | 253 | << handler.second->alias() |
550 | 253 | << "</a></li>" << "\n"; |
551 | 253 | } |
552 | 2.52k | } |
553 | 69 | (*output) << NAVIGATION_BAR_SUFFIX; |
554 | | |
555 | 69 | if (!static_pages_available()) { |
556 | 0 | (*output) << "<div style=\"color: red\"><strong>" |
557 | 0 | << "Static pages not available. Configure YB_HOME or use the --webserver_doc_root " |
558 | 0 | << "flag to fix page styling.</strong></div>\n"; |
559 | 0 | } |
560 | 69 | } |
561 | | |
562 | 18.1k | bool Webserver::static_pages_available() const { |
563 | 18.1k | return !opts_.doc_root.empty() && opts_.enable_doc_root; |
564 | 18.1k | } |
565 | | |
566 | 17.1k | void Webserver::set_footer_html(const std::string& html) { |
567 | 17.1k | std::lock_guard<std::shared_timed_mutex> l(lock_); |
568 | 17.1k | footer_html_ = html; |
569 | 17.1k | } |
570 | | |
571 | 69 | void Webserver::BootstrapPageFooter(stringstream* output) { |
572 | 69 | SharedLock<std::shared_timed_mutex> l(lock_); |
573 | 69 | *output << "<div class='yb-bottom-spacer'></div></div>\n"; // end bootstrap 'container' div |
574 | 69 | if (!footer_html_.empty()) { |
575 | 60 | *output << "<footer class='footer'><div class='yb-footer container text-muted'>"; |
576 | 60 | *output << footer_html_; |
577 | 60 | *output << "</div></footer>"; |
578 | 60 | } |
579 | 69 | *output << "</body></html>"; |
580 | 69 | } |
581 | | |
582 | 527 | void Webserver::EnterWorkerThreadCallbackStatic() { |
583 | 527 | cds::threading::Manager::attachThread(); |
584 | 527 | } |
585 | | |
586 | 265 | void Webserver::LeaveWorkerThreadCallbackStatic() { |
587 | 265 | cds::threading::Manager::detachThread(); |
588 | 265 | } |
589 | | |
590 | | } // namespace yb |