YugabyteDB (2.13.0.0-b42, bfc6a6643e7399ac8a0e81d06a3ee6d6571b33ab)

Coverage Report

Created: 2022-03-09 17:30

/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