Commit 06921f35 authored by Tatsuhiro Tsujikawa's avatar Tatsuhiro Tsujikawa

nghttpx: Restructure mode settings

It is very hard to support multiple protocols in backend while
retaining multiple mode settings.  Therefore, we dropped modes except
for default and HTTP/2 proxy mode.  The other removed modes can be
emulated using combinations of options.  Now the backend connection is
not encrypted by default.  To enable encryption on backend connection,
use --backend-tls option.
parent 44d38017
......@@ -125,7 +125,8 @@ OPTIONS = [
"backend-address-family",
"frontend-http2-max-concurrent-streams",
"backend-http2-max-concurrent-streams",
"backend-connections-per-frontend"
"backend-connections-per-frontend",
"backend-tls"
]
LOGVARS = [
......
......@@ -1416,9 +1416,9 @@ int lookup_method_token(const uint8_t *name, size_t namelen) {
return -1;
}
const char *to_method_string(int method_token) {
StringRef to_method_string(int method_token) {
// we happened to use same value for method with http-parser.
return http_method_str(static_cast<http_method>(method_token));
return StringRef{http_method_str(static_cast<http_method>(method_token))};
}
int get_pure_path_component(const char **base, size_t *baselen,
......
......@@ -322,7 +322,11 @@ bool expect_response_body(int status_code);
int lookup_method_token(const uint8_t *name, size_t namelen);
int lookup_method_token(const std::string &name);
const char *to_method_string(int method_token);
// Returns string representation of |method_token|. This is wrapper
// function over http_method_str from http-parser. If |method_token|
// is not known to http-parser, "<unknown>" is returned. The returned
// StringRef is guaranteed to be NULL-terminated.
StringRef to_method_string(int method_token);
template <typename InputIt>
std::string normalize_path(InputIt first, InputIt last) {
......
This diff is collapsed.
......@@ -853,11 +853,11 @@ void ClientHandler::write_accesslog(Downstream *downstream) {
upstream_accesslog(
get_config()->logging.access.format,
LogSpec{
downstream, StringRef(ipaddr_), http2::to_method_string(req.method),
downstream, StringRef{ipaddr_}, http2::to_method_string(req.method),
req.method == HTTP_CONNECT
? StringRef(req.authority)
: (get_config()->http2_proxy || get_config()->client_proxy)
: get_config()->http2_proxy
? StringRef(construct_absolute_request_uri(req))
: req.path.empty()
? req.method == HTTP_OPTIONS
......
......@@ -571,33 +571,70 @@ int parse_duration(ev_tstamp *dest, const char *opt, const char *optarg) {
} // namespace
namespace {
// Parses host-path mapping patterns in |src|, and stores mappings in
// config. We will store each host-path pattern found in |src| with
// |addr|. |addr| will be copied accordingly. Also we make a group
// based on the pattern. The "/" pattern is considered as catch-all.
void parse_mapping(const DownstreamAddrConfig &addr, const char *src) {
// Parses host-path mapping patterns in |src_pattern|, and stores
// mappings in config. We will store each host-path pattern found in
// |src| with |addr|. |addr| will be copied accordingly. Also we
// make a group based on the pattern. The "/" pattern is considered
// as catch-all. We also parse protocol specified in |src_proto|.
//
// This function returns 0 if it succeeds, or -1.
int parse_mapping(const DownstreamAddrConfig &addr,
const StringRef &src_pattern, const StringRef &src_proto) {
// This returns at least 1 element (it could be empty string). We
// will append '/' to all patterns, so it becomes catch-all pattern.
auto mapping = util::split_config_str_list(src, ':');
auto mapping = util::split_str(src_pattern, ':');
assert(!mapping.empty());
auto &addr_groups = mod_config()->conn.downstream.addr_groups;
auto proto = PROTO_HTTP1;
if (!src_proto.empty()) {
if (!util::istarts_with_l(src_proto, "proto=")) {
LOG(ERROR) << "backend: proto keyword not found";
return -1;
}
auto protostr = StringRef{std::begin(src_proto) + str_size("proto="),
std::end(src_proto)};
if (protostr.empty()) {
LOG(ERROR) << "backend: protocol is empty";
return -1;
}
if (util::streq_l("h2", std::begin(protostr), protostr.size())) {
proto = PROTO_HTTP2;
} else if (util::streq_l("http/1.1", std::begin(protostr),
protostr.size())) {
proto = PROTO_HTTP1;
} else {
LOG(ERROR) << "backend: unknown protocol " << protostr;
return -1;
}
}
for (const auto &raw_pattern : mapping) {
auto done = false;
std::string pattern;
auto slash = std::find(raw_pattern.first, raw_pattern.second, '/');
if (slash == raw_pattern.second) {
auto slash = std::find(std::begin(raw_pattern), std::end(raw_pattern), '/');
if (slash == std::end(raw_pattern)) {
// This effectively makes empty pattern to "/".
pattern.assign(raw_pattern.first, raw_pattern.second);
pattern.assign(std::begin(raw_pattern), std::end(raw_pattern));
util::inp_strlower(pattern);
pattern += '/';
} else {
pattern.assign(raw_pattern.first, slash);
pattern.assign(std::begin(raw_pattern), slash);
util::inp_strlower(pattern);
pattern += http2::normalize_path(slash, raw_pattern.second);
pattern += http2::normalize_path(slash, std::end(raw_pattern));
}
for (auto &g : addr_groups) {
if (g.pattern == pattern) {
if (g.proto != proto) {
LOG(ERROR) << "backend: protocol mismatch. We saw protocol "
<< strproto(g.proto) << " for pattern " << g.pattern
<< ", but another protocol " << strproto(proto);
return -1;
}
g.addrs.push_back(addr);
done = true;
break;
......@@ -608,11 +645,13 @@ void parse_mapping(const DownstreamAddrConfig &addr, const char *src) {
}
DownstreamAddrGroupConfig g(StringRef{pattern});
g.addrs.push_back(addr);
g.proto = proto;
mod_config()->router.add_route(StringRef{g.pattern}, addr_groups.size());
addr_groups.push_back(std::move(g));
}
return 0;
}
} // namespace
......@@ -670,6 +709,7 @@ enum {
SHRPX_OPTID_BACKEND_READ_TIMEOUT,
SHRPX_OPTID_BACKEND_REQUEST_BUFFER,
SHRPX_OPTID_BACKEND_RESPONSE_BUFFER,
SHRPX_OPTID_BACKEND_TLS,
SHRPX_OPTID_BACKEND_TLS_SNI_FIELD,
SHRPX_OPTID_BACKEND_WRITE_TIMEOUT,
SHRPX_OPTID_BACKLOG,
......@@ -914,6 +954,11 @@ int option_lookup_token(const char *name, size_t namelen) {
break;
case 11:
switch (name[10]) {
case 's':
if (util::strieq_l("backend-tl", name, 10)) {
return SHRPX_OPTID_BACKEND_TLS;
}
break;
case 't':
if (util::strieq_l("write-burs", name, 10)) {
return SHRPX_OPTID_WRITE_BURST;
......@@ -1495,19 +1540,17 @@ int parse_config(const char *opt, const char *optarg,
switch (optid) {
case SHRPX_OPTID_BACKEND: {
auto optarglen = strlen(optarg);
const char *pat_delim = strchr(optarg, ';');
if (!pat_delim) {
pat_delim = optarg + optarglen;
}
auto src = StringRef{optarg};
auto addr_end = std::find(std::begin(src), std::end(src), ';');
DownstreamAddrConfig addr{};
if (util::istarts_with(optarg, SHRPX_UNIX_PATH_PREFIX)) {
auto path = optarg + str_size(SHRPX_UNIX_PATH_PREFIX);
addr.host = ImmutableString(path, pat_delim);
auto path = std::begin(src) + str_size(SHRPX_UNIX_PATH_PREFIX);
addr.host = ImmutableString(path, addr_end);
addr.host_unix = true;
} else {
if (split_host_port(host, sizeof(host), &port, optarg,
pat_delim - optarg) == -1) {
if (split_host_port(host, sizeof(host), &port, &src[0],
addr_end - std::begin(src)) == -1) {
return -1;
}
......@@ -1515,14 +1558,16 @@ int parse_config(const char *opt, const char *optarg,
addr.port = port;
}
auto mapping = pat_delim < optarg + optarglen ? pat_delim + 1 : pat_delim;
// We may introduce new parameter after additional ';', so don't
// allow extra ';' in pattern for now.
if (strchr(mapping, ';') != nullptr) {
LOG(ERROR) << opt << ": ';' must not be used in pattern";
auto mapping = addr_end == std::end(src) ? addr_end : addr_end + 1;
auto mapping_end = std::find(mapping, std::end(src), ';');
auto proto = mapping_end == std::end(src) ? mapping_end : mapping_end + 1;
auto proto_end = std::find(proto, std::end(src), ';');
if (parse_mapping(addr, StringRef{mapping, mapping_end},
StringRef{proto, proto_end}) != 0) {
return -1;
}
parse_mapping(addr, mapping);
return 0;
}
......@@ -1607,13 +1652,13 @@ int parse_config(const char *opt, const char *optarg,
return 0;
case SHRPX_OPTID_HTTP2_BRIDGE:
mod_config()->http2_bridge = util::strieq(optarg, "yes");
return 0;
LOG(ERROR) << opt << ": deprecated. Use backend=<addr>,<port>;;proto=h2 "
"and backend-tls";
return -1;
case SHRPX_OPTID_CLIENT_PROXY:
mod_config()->client_proxy = util::strieq(optarg, "yes");
return 0;
LOG(ERROR) << opt << ": deprecated. Use http2-proxy, frontend-no-tls, "
"backend=<addr>,<port>;;proto=h2 and backend-tls";
return -1;
case SHRPX_OPTID_ADD_X_FORWARDED_FOR:
mod_config()->http.xff.add = util::strieq(optarg, "yes");
......@@ -1746,8 +1791,8 @@ int parse_config(const char *opt, const char *optarg,
return 0;
case SHRPX_OPTID_BACKEND_NO_TLS:
mod_config()->conn.downstream.no_tls = util::strieq(optarg, "yes");
LOG(WARN) << opt << ": deprecated. backend connection is not encrypted by "
"default. See also " << SHRPX_OPT_BACKEND_TLS;
return 0;
case SHRPX_OPTID_BACKEND_TLS_SNI_FIELD:
mod_config()->tls.backend_sni_name = optarg;
......@@ -1834,9 +1879,9 @@ int parse_config(const char *opt, const char *optarg,
return 0;
case SHRPX_OPTID_CLIENT:
mod_config()->client = util::strieq(optarg, "yes");
return 0;
LOG(ERROR) << opt << ": deprecated. Use frontend-no-tls, "
"backend=<addr>,<port>;;proto=h2 and backend-tls";
return -1;
case SHRPX_OPTID_INSECURE:
mod_config()->tls.insecure = util::strieq(optarg, "yes");
......@@ -2312,7 +2357,11 @@ int parse_config(const char *opt, const char *optarg,
return 0;
case SHRPX_OPTID_BACKEND_HTTP1_TLS:
mod_config()->conn.downstream.http1_tls = util::strieq(optarg, "yes");
LOG(WARN) << opt << ": deprecated. Use " << SHRPX_OPT_BACKEND_TLS
<< " instead.";
// fall through
case SHRPX_OPTID_BACKEND_TLS:
mod_config()->conn.downstream.no_tls = !util::strieq(optarg, "yes");
return 0;
case SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_TLS:
......@@ -2533,4 +2582,17 @@ int int_syslog_facility(const char *strfacility) {
return -1;
}
StringRef strproto(shrpx_proto proto) {
switch (proto) {
case PROTO_NONE:
return StringRef::from_lit("none");
case PROTO_HTTP1:
return StringRef::from_lit("http/1.1");
case PROTO_HTTP2:
return StringRef::from_lit("h2");
case PROTO_MEMCACHED:
return StringRef::from_lit("memcached");
}
}
} // namespace shrpx
......@@ -234,6 +234,7 @@ constexpr char SHRPX_OPT_BACKEND_HTTP2_MAX_CONCURRENT_STREAMS[] =
"backend-http2-max-concurrent-streams";
constexpr char SHRPX_OPT_BACKEND_CONNECTIONS_PER_FRONTEND[] =
"backend-connections-per-frontend";
constexpr char SHRPX_OPT_BACKEND_TLS[] = "backend-tls";
constexpr size_t SHRPX_OBFUSCATED_NODE_LENGTH = 8;
......@@ -599,11 +600,6 @@ struct Config {
bool verbose;
bool daemon;
bool http2_proxy;
bool http2_bridge;
bool client_proxy;
bool client;
// true if --client or --client-proxy are enabled.
bool client_mode;
};
const Config *get_config();
......@@ -650,6 +646,9 @@ std::unique_ptr<TicketKeys>
read_tls_ticket_key_file(const std::vector<std::string> &files,
const EVP_CIPHER *cipher, const EVP_MD *hmac);
// Returns string representation of |proto|.
StringRef strproto(shrpx_proto proto);
} // namespace shrpx
#endif // SHRPX_CONFIG_H
......@@ -264,9 +264,9 @@ int Http2DownstreamConnection::push_request_headers() {
auto &httpconf = get_config()->http;
auto &http2conf = get_config()->http2;
auto no_host_rewrite =
httpconf.no_host_rewrite || get_config()->http2_proxy ||
get_config()->client_proxy || req.method == HTTP_CONNECT;
auto no_host_rewrite = httpconf.no_host_rewrite ||
get_config()->http2_proxy ||
req.method == HTTP_CONNECT;
// http2session_ has already in CONNECTED state, so we can get
// addr_idx here.
......@@ -302,7 +302,7 @@ int Http2DownstreamConnection::push_request_headers() {
httpconf.add_request_headers.size());
nva.push_back(
http2::make_nv_lc_nocopy(":method", http2::to_method_string(req.method)));
http2::make_nv_ls_nocopy(":method", http2::to_method_string(req.method)));
if (req.method != HTTP_CONNECT) {
assert(!req.scheme.empty());
......@@ -350,8 +350,7 @@ int Http2DownstreamConnection::push_request_headers() {
if (fwdconf.params) {
auto params = fwdconf.params;
if (get_config()->http2_proxy || get_config()->client_proxy ||
req.method == HTTP_CONNECT) {
if (get_config()->http2_proxy || req.method == HTTP_CONNECT) {
params &= ~FORWARDED_PROTO;
}
......@@ -394,8 +393,7 @@ int Http2DownstreamConnection::push_request_headers() {
nva.push_back(http2::make_nv_ls_nocopy("x-forwarded-for", (*xff).value));
}
if (!get_config()->http2_proxy && !get_config()->client_proxy &&
req.method != HTTP_CONNECT) {
if (!get_config()->http2_proxy && req.method != HTTP_CONNECT) {
// We use same protocol with :scheme header field
nva.push_back(http2::make_nv_ls_nocopy("x-forwarded-proto", req.scheme));
}
......
......@@ -1460,8 +1460,7 @@ int Http2Session::connection_made() {
entry[1].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE;
entry[1].value = (1 << http2conf.downstream.window_bits) - 1;
if (http2conf.no_server_push || get_config()->http2_proxy ||
get_config()->client_proxy) {
if (http2conf.no_server_push || get_config()->http2_proxy) {
entry[nentry].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH;
entry[nentry].value = 0;
++nentry;
......
......@@ -316,7 +316,7 @@ int Http2Upstream::on_request_headers(Downstream *downstream,
if (path) {
if (method_token == HTTP_OPTIONS && path->value == "*") {
// Server-wide OPTIONS request. Path is empty.
} else if (get_config()->http2_proxy || get_config()->client_proxy) {
} else if (get_config()->http2_proxy) {
req.path = http2::value_to_str(path);
} else {
const auto &value = path->value;
......@@ -1346,8 +1346,7 @@ int Http2Upstream::on_downstream_header_complete(Downstream *downstream) {
auto &httpconf = get_config()->http;
if (!get_config()->http2_proxy && !get_config()->client_proxy &&
!httpconf.no_location_rewrite) {
if (!get_config()->http2_proxy && !httpconf.no_location_rewrite) {
downstream->rewrite_location_response_header(req.scheme);
}
......@@ -1416,7 +1415,7 @@ int Http2Upstream::on_downstream_header_complete(Downstream *downstream) {
http2::copy_headers_to_nva_nocopy(nva, resp.fs.headers());
if (!get_config()->http2_proxy && !get_config()->client_proxy) {
if (!get_config()->http2_proxy) {
nva.push_back(http2::make_nv_ls_nocopy("server", httpconf.server_name));
} else {
auto server = resp.fs.header(http2::HD_SERVER);
......@@ -1489,9 +1488,8 @@ int Http2Upstream::on_downstream_header_complete(Downstream *downstream) {
if (!http2conf.no_server_push &&
nghttp2_session_get_remote_settings(session_,
NGHTTP2_SETTINGS_ENABLE_PUSH) == 1 &&
!get_config()->http2_proxy && !get_config()->client_proxy &&
(downstream->get_stream_id() % 2) && resp.fs.header(http2::HD_LINK) &&
resp.http_status == 200 &&
!get_config()->http2_proxy && (downstream->get_stream_id() % 2) &&
resp.fs.header(http2::HD_LINK) && resp.http_status == 200 &&
(req.method == HTTP_GET || req.method == HTTP_POST)) {
if (prepare_push_promise(downstream) != 0) {
......@@ -1852,7 +1850,7 @@ bool Http2Upstream::push_enabled() const {
return !(get_config()->http2.no_server_push ||
nghttp2_session_get_remote_settings(
session_, NGHTTP2_SETTINGS_ENABLE_PUSH) == 0 ||
get_config()->http2_proxy || get_config()->client_proxy);
get_config()->http2_proxy);
}
int Http2Upstream::initiate_push(Downstream *downstream, const char *uri,
......
......@@ -279,9 +279,8 @@ int HttpDownstreamConnection::push_request_headers() {
// For HTTP/1.0 request, there is no authority in request. In that
// case, we use backend server's host nonetheless.
auto authority = StringRef(downstream_hostport);
auto no_host_rewrite = httpconf.no_host_rewrite ||
get_config()->http2_proxy ||
get_config()->client_proxy || connect_method;
auto no_host_rewrite =
httpconf.no_host_rewrite || get_config()->http2_proxy || connect_method;
if (no_host_rewrite && !req.authority.empty()) {
authority = StringRef(req.authority);
......@@ -293,12 +292,12 @@ int HttpDownstreamConnection::push_request_headers() {
// Assume that method and request path do not contain \r\n.
auto meth = http2::to_method_string(req.method);
buf->append(meth, strlen(meth));
buf->append(meth);
buf->append(" ");
if (connect_method) {
buf->append(authority);
} else if (get_config()->http2_proxy || get_config()->client_proxy) {
} else if (get_config()->http2_proxy) {
// Construct absolute-form request target because we are going to
// send a request to a HTTP/1 proxy.
assert(!req.scheme.empty());
......@@ -363,8 +362,7 @@ int HttpDownstreamConnection::push_request_headers() {
if (fwdconf.params) {
auto params = fwdconf.params;
if (get_config()->http2_proxy || get_config()->client_proxy ||
connect_method) {
if (get_config()->http2_proxy || connect_method) {
params &= ~FORWARDED_PROTO;
}
......@@ -407,8 +405,7 @@ int HttpDownstreamConnection::push_request_headers() {
buf->append((*xff).value);
buf->append("\r\n");
}
if (!get_config()->http2_proxy && !get_config()->client_proxy &&
!connect_method) {
if (!get_config()->http2_proxy && !connect_method) {
buf->append("X-Forwarded-Proto: ");
assert(!req.scheme.empty());
buf->append(req.scheme);
......
......@@ -232,7 +232,7 @@ void rewrite_request_host_path_from_uri(Request &req, const char *uri,
path += '?';
path.append(uri + fdata.off, fdata.len);
}
if (get_config()->http2_proxy || get_config()->client_proxy) {
if (get_config()->http2_proxy) {
req.path = std::move(path);
} else {
req.path = http2::rewrite_clean_path(std::begin(path), std::end(path));
......@@ -306,7 +306,7 @@ int htp_hdrs_completecb(http_parser *htp) {
}
// checking UF_HOST could be redundant, but just in case ...
if (!(u.field_set & (1 << UF_SCHEMA)) || !(u.field_set & (1 << UF_HOST))) {
if (get_config()->http2_proxy || get_config()->client_proxy) {
if (get_config()->http2_proxy) {
// Request URI should be absolute-form for client proxy mode
return -1;
}
......@@ -929,8 +929,7 @@ int HttpsUpstream::on_downstream_header_complete(Downstream *downstream) {
auto &httpconf = get_config()->http;
if (!get_config()->http2_proxy && !get_config()->client_proxy &&
!httpconf.no_location_rewrite) {
if (!get_config()->http2_proxy && !httpconf.no_location_rewrite) {
downstream->rewrite_location_response_header(
get_client_handler()->get_upstream_scheme());
}
......@@ -999,7 +998,7 @@ int HttpsUpstream::on_downstream_header_complete(Downstream *downstream) {
}
}
if (!get_config()->http2_proxy && !get_config()->client_proxy) {
if (!get_config()->http2_proxy) {
buf->append("Server: ");
buf->append(httpconf.server_name);
buf->append("\r\n");
......
......@@ -70,7 +70,7 @@ mrb_value request_get_method(mrb_state *mrb, mrb_value self) {
const auto &req = downstream->request();
auto method = http2::to_method_string(req.method);
return mrb_str_new_cstr(mrb, method);
return mrb_str_new(mrb, method.c_str(), method.size());
}
} // namespace
......
......@@ -262,7 +262,7 @@ void on_ctrl_recv_callback(spdylay_session *session, spdylay_frame_type type,
} else {
req.scheme = scheme->value;
req.authority = host->value;
if (get_config()->http2_proxy || get_config()->client_proxy) {
if (get_config()->http2_proxy) {
req.path = path->value;
} else if (method_token == HTTP_OPTIONS && path->value == "*") {
// Server-wide OPTIONS request. Path is empty.
......@@ -1009,8 +1009,7 @@ int SpdyUpstream::on_downstream_header_complete(Downstream *downstream) {
auto &httpconf = get_config()->http;
if (!get_config()->http2_proxy && !get_config()->client_proxy &&
!httpconf.no_location_rewrite) {
if (!get_config()->http2_proxy && !httpconf.no_location_rewrite) {
downstream->rewrite_location_response_header(req.scheme);
}
......@@ -1044,7 +1043,7 @@ int SpdyUpstream::on_downstream_header_complete(Downstream *downstream) {
nv[hdidx++] = hd.value.c_str();
}
if (!get_config()->http2_proxy && !get_config()->client_proxy) {
if (!get_config()->http2_proxy) {
nv[hdidx++] = "server";
nv[hdidx++] = httpconf.server_name.c_str();
} else {
......
......@@ -318,7 +318,7 @@ size_t match_downstream_addr_group_host(
return group;
}
group = router.match("", path);
group = router.match(StringRef::from_lit(""), path);
if (group != -1) {
if (LOG_ENABLED(INFO)) {
LOG(INFO) << "Found pattern with query " << path
......
......@@ -400,7 +400,7 @@ public:
explicit StringRef(const std::string &s) : base(s.c_str()), len(s.size()) {}
explicit StringRef(const ImmutableString &s)
: base(s.c_str()), len(s.size()) {}
StringRef(const char *s) : base(s), len(strlen(s)) {}
explicit StringRef(const char *s) : base(s), len(strlen(s)) {}
template <typename CharT>
constexpr StringRef(const CharT *s, size_t n)
: base(reinterpret_cast<const char *>(s)), len(n) {}
......
......@@ -857,6 +857,27 @@ std::vector<unsigned char> get_default_alpn() {
return res;
}
std::vector<StringRef> split_str(const StringRef &s, char delim) {
size_t len = 1;
auto last = std::end(s);
for (auto first = std::begin(s), d = first;
(d = std::find(first, last, delim)) != last; ++len, first = d + 1)
;
auto list = std::vector<StringRef>(len);
len = 0;
for (auto first = std::begin(s);; ++len) {
auto stop = std::find(first, last, delim);
list[len] = StringRef{first, stop};
if (stop == last) {
break;
}
first = stop + 1;
}
return list;
}
std::vector<Range<const char *>> split_config_str_list(const char *s,
char delim) {
size_t len = 1;
......
......@@ -225,6 +225,11 @@ bool istarts_with_l(const std::string &a, const CharT(&b)[N]) {
return istarts_with(std::begin(a), std::end(a), b, b + N - 1);
}
template <typename CharT, size_t N>
bool istarts_with_l(const StringRef &a, const CharT(&b)[N]) {
return istarts_with(std::begin(a), std::end(a), b, b + N - 1);
}
template <typename InputIterator1, typename InputIterator2>
bool ends_with(InputIterator1 first1, InputIterator1 last1,
InputIterator2 first2, InputIterator2 last2) {
......@@ -543,6 +548,11 @@ std::vector<std::string> parse_config_str_list(const char *s, char delim = ',');
std::vector<Range<const char *>> split_config_str_list(const char *s,
char delim);
// Parses delimited strings in |s| and returns Substrings in |s|
// delimited by |delim|. The any white spaces around substring are
// treated as a part of substring.
std::vector<StringRef> split_str(const StringRef &s, char delim);
// Returns given time |tp| in Common Log format (e.g.,
// 03/Jul/2014:00:19:38 +0900)
// Expected type of |tp| is std::chrono::timepoint
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment