diff --git a/gennghttpxfun.py b/gennghttpxfun.py index abc74025..b7c31d87 100755 --- a/gennghttpxfun.py +++ b/gennghttpxfun.py @@ -179,6 +179,7 @@ OPTIONS = [ "no-strip-incoming-early-data", "bpf-program-file", "no-bpf", + "http2-altsvc", ] LOGVARS = [ diff --git a/src/shrpx-unittest.cc b/src/shrpx-unittest.cc index 3cf38163..53b3ff03 100644 --- a/src/shrpx-unittest.cc +++ b/src/shrpx-unittest.cc @@ -135,6 +135,8 @@ int main(int argc, char *argv[]) { shrpx::test_shrpx_http_create_via_header_value) || !CU_add_test(pSuite, "http_create_affinity_cookie", shrpx::test_shrpx_http_create_affinity_cookie) || + !CU_add_test(pSuite, "http_create_atlsvc_header_field_value", + shrpx::test_shrpx_http_create_altsvc_header_value) || !CU_add_test(pSuite, "router_match", shrpx::test_shrpx_router_match) || !CU_add_test(pSuite, "router_match_wildcard", shrpx::test_shrpx_router_match_wildcard) || @@ -197,6 +199,7 @@ int main(int argc, char *argv[]) { shrpx::test_util_extract_host) || !CU_add_test(pSuite, "util_split_hostport", shrpx::test_util_split_hostport) || + !CU_add_test(pSuite, "util_split_str", shrpx::test_util_split_str) || !CU_add_test(pSuite, "gzip_inflate", test_nghttp2_gzip_inflate) || !CU_add_test(pSuite, "buffer_write", nghttp2::test_buffer_write) || !CU_add_test(pSuite, "pool_recycle", nghttp2::test_pool_recycle) || diff --git a/src/shrpx.cc b/src/shrpx.cc index cf913390..5020e38f 100644 --- a/src/shrpx.cc +++ b/src/shrpx.cc @@ -87,6 +87,7 @@ #include "shrpx_signal.h" #include "shrpx_connection.h" #include "shrpx_log.h" +#include "shrpx_http.h" #include "util.h" #include "app_helper.h" #include "tls.h" @@ -2723,13 +2724,18 @@ HTTP: Rewrite host and :authority header fields in default mode. When --http2-proxy is used, these headers will not be altered regardless of this option. - --altsvc= + --altsvc= Specify protocol ID, port, host and origin of - alternative service. and are optional. - They are advertised in alt-svc header field only in - HTTP/1.1 frontend. This option can be used multiple - times to specify multiple alternative services. - Example: --altsvc=h2,443 + alternative service. , and are + optional. Empty and are allowed and + they are treated as nothing is specified. They are + advertised in alt-svc header field only in HTTP/1.1 + frontend. This option can be used multiple times to + specify multiple alternative services. + Example: --altsvc="h2,443,,,ma=3600; persist=1' + --http2-altsvc= + Just like --altsvc option, but this altsvc is only sent + in HTTP/2 frontend. --add-request-header=
Specify additional header field to add to request header set. This option just appends header field and won't @@ -3183,6 +3189,16 @@ int process_options(Config *config, config->http2.upstream.callbacks = create_http2_upstream_callbacks(); config->http2.downstream.callbacks = create_http2_downstream_callbacks(); + if (!config->http.altsvcs.empty()) { + config->http.altsvc_header_value = + http::create_altsvc_header_value(config->balloc, config->http.altsvcs); + } + + if (!config->http.http2_altsvcs.empty()) { + config->http.http2_altsvc_header_value = http::create_altsvc_header_value( + config->balloc, config->http.http2_altsvcs); + } + return 0; } } // namespace @@ -3571,6 +3587,7 @@ int main(int argc, char **argv) { &flag, 168}, {SHRPX_OPT_BPF_PROGRAM_FILE.c_str(), required_argument, &flag, 169}, {SHRPX_OPT_NO_BPF.c_str(), no_argument, &flag, 170}, + {SHRPX_OPT_HTTP2_ALTSVC.c_str(), required_argument, &flag, 171}, {nullptr, 0, nullptr, 0}}; int option_index = 0; @@ -4382,6 +4399,10 @@ int main(int argc, char **argv) { // --no-bpf cmdcfgs.emplace_back(SHRPX_OPT_NO_BPF, StringRef::from_lit("yes")); break; + case 171: + // --http2-altsvc + cmdcfgs.emplace_back(SHRPX_OPT_HTTP2_ALTSVC, StringRef{optarg}); + break; default: break; } diff --git a/src/shrpx_config.cc b/src/shrpx_config.cc index 9a127a9c..3c0a63dc 100644 --- a/src/shrpx_config.cc +++ b/src/shrpx_config.cc @@ -372,6 +372,57 @@ int parse_int(T *dest, const StringRef &opt, const char *optarg) { return 0; } +namespace { +int parse_altsvc(AltSvc &altsvc, const StringRef &opt, + const StringRef &optarg) { + // PROTOID, PORT, HOST, ORIGIN, PARAMS. + auto tokens = util::split_str(optarg, ',', 5); + + if (tokens.size() < 2) { + // Requires at least protocol_id and port + LOG(ERROR) << opt << ": too few parameters: " << optarg; + return -1; + } + + int port; + + if (parse_uint(&port, opt, tokens[1]) != 0) { + return -1; + } + + if (port < 1 || + port > static_cast(std::numeric_limits::max())) { + LOG(ERROR) << opt << ": port is invalid: " << tokens[1]; + return -1; + } + + altsvc.protocol_id = make_string_ref(config->balloc, tokens[0]); + + altsvc.port = port; + altsvc.service = make_string_ref(config->balloc, tokens[1]); + + if (tokens.size() > 2) { + if (!tokens[2].empty()) { + altsvc.host = make_string_ref(config->balloc, tokens[2]); + } + + if (tokens.size() > 3) { + if (!tokens[3].empty()) { + altsvc.origin = make_string_ref(config->balloc, tokens[3]); + } + + if (tokens.size() > 4) { + if (!tokens[4].empty()) { + altsvc.params = make_string_ref(config->balloc, tokens[4]); + } + } + } + } + + return 0; +} +} // namespace + namespace { // generated by gennghttpxfun.py LogFragmentType log_var_lookup_token(const char *name, size_t namelen) { @@ -1863,6 +1914,11 @@ int option_lookup_token(const char *name, size_t namelen) { return SHRPX_OPTID_BACKEND_IPV6; } break; + case 'c': + if (util::strieq_l("http2-altsv", name, 11)) { + return SHRPX_OPTID_HTTP2_ALTSVC; + } + break; case 'e': if (util::strieq_l("host-rewrit", name, 11)) { return SHRPX_OPTID_HOST_REWRITE; @@ -3186,45 +3242,10 @@ int parse_config(Config *config, int optid, const StringRef &opt, case SHRPX_OPTID_PADDING: return parse_uint(&config->padding, opt, optarg); case SHRPX_OPTID_ALTSVC: { - auto tokens = util::split_str(optarg, ','); - - if (tokens.size() < 2) { - // Requires at least protocol_id and port - LOG(ERROR) << opt << ": too few parameters: " << optarg; - return -1; - } - - if (tokens.size() > 4) { - // We only need protocol_id, port, host and origin - LOG(ERROR) << opt << ": too many parameters: " << optarg; - return -1; - } - - int port; - - if (parse_uint(&port, opt, tokens[1]) != 0) { - return -1; - } - - if (port < 1 || - port > static_cast(std::numeric_limits::max())) { - LOG(ERROR) << opt << ": port is invalid: " << tokens[1]; - return -1; - } - AltSvc altsvc{}; - altsvc.protocol_id = make_string_ref(config->balloc, tokens[0]); - - altsvc.port = port; - altsvc.service = make_string_ref(config->balloc, tokens[1]); - - if (tokens.size() > 2) { - altsvc.host = make_string_ref(config->balloc, tokens[2]); - - if (tokens.size() > 3) { - altsvc.origin = make_string_ref(config->balloc, tokens[3]); - } + if (parse_altsvc(altsvc, opt, optarg) != 0) { + return -1; } config->http.altsvcs.push_back(std::move(altsvc)); @@ -3841,6 +3862,17 @@ int parse_config(Config *config, int optid, const StringRef &opt, #endif // ENABLE_HTTP3 return 0; + case SHRPX_OPTID_HTTP2_ALTSVC: { + AltSvc altsvc{}; + + if (parse_altsvc(altsvc, opt, optarg) != 0) { + return -1; + } + + config->http.http2_altsvcs.push_back(std::move(altsvc)); + + return 0; + } case SHRPX_OPTID_CONF: LOG(WARN) << "conf: ignored"; diff --git a/src/shrpx_config.h b/src/shrpx_config.h index d6466fcc..4b1bd8fd 100644 --- a/src/shrpx_config.h +++ b/src/shrpx_config.h @@ -366,6 +366,7 @@ constexpr auto SHRPX_OPT_NO_STRIP_INCOMING_EARLY_DATA = constexpr auto SHRPX_OPT_BPF_PROGRAM_FILE = StringRef::from_lit("bpf-program-file"); constexpr auto SHRPX_OPT_NO_BPF = StringRef::from_lit("no-bpf"); +constexpr auto SHRPX_OPT_HTTP2_ALTSVC = StringRef::from_lit("http2-altsvc"); constexpr size_t SHRPX_OBFUSCATED_NODE_LENGTH = 8; @@ -426,7 +427,7 @@ enum class ForwardedNode { }; struct AltSvc { - StringRef protocol_id, host, origin, service; + StringRef protocol_id, host, origin, service, params; uint16_t port; }; @@ -771,6 +772,11 @@ struct HttpConfig { bool strip_incoming; } early_data; std::vector altsvcs; + // altsvcs serialized in a wire format. + StringRef altsvc_header_value; + std::vector http2_altsvcs; + // http2_altsvcs serialized in a wire format. + StringRef http2_altsvc_header_value; std::vector error_pages; HeaderRefs add_request_headers; HeaderRefs add_response_headers; @@ -1161,6 +1167,7 @@ enum { SHRPX_OPTID_FRONTEND_WRITE_TIMEOUT, SHRPX_OPTID_HEADER_FIELD_BUFFER, SHRPX_OPTID_HOST_REWRITE, + SHRPX_OPTID_HTTP2_ALTSVC, SHRPX_OPTID_HTTP2_BRIDGE, SHRPX_OPTID_HTTP2_MAX_CONCURRENT_STREAMS, SHRPX_OPTID_HTTP2_NO_COOKIE_CRUMBLING, diff --git a/src/shrpx_http.cc b/src/shrpx_http.cc index 69bc3236..0627250f 100644 --- a/src/shrpx_http.cc +++ b/src/shrpx_http.cc @@ -220,6 +220,57 @@ bool require_cookie_secure_attribute(SessionAffinityCookieSecure secure, } } +StringRef create_altsvc_header_value(BlockAllocator &balloc, + const std::vector &altsvcs) { + // =":"; + size_t len = 0; + + if (altsvcs.empty()) { + return StringRef{}; + } + + for (auto &altsvc : altsvcs) { + len += util::percent_encode_tokenlen(altsvc.protocol_id); + len += str_size("=\""); + len += util::quote_stringlen(altsvc.host); + len += str_size(":"); + len += altsvc.service.size(); + len += str_size("\""); + if (!altsvc.params.empty()) { + len += str_size("; "); + len += altsvc.params.size(); + } + } + + // ", " between items. + len += (altsvcs.size() - 1) * 2; + + // We will write additional ", " at the end, and cut it later. + auto iov = make_byte_ref(balloc, len + 2); + auto p = iov.base; + + for (auto &altsvc : altsvcs) { + p = util::percent_encode_token(p, altsvc.protocol_id); + p = util::copy_lit(p, "=\""); + p = util::quote_string(p, altsvc.host); + *p++ = ':'; + p = std::copy(std::begin(altsvc.service), std::end(altsvc.service), p); + *p++ = '"'; + if (!altsvc.params.empty()) { + p = util::copy_lit(p, "; "); + p = std::copy(std::begin(altsvc.params), std::end(altsvc.params), p); + } + p = util::copy_lit(p, ", "); + } + + p -= 2; + *p = '\0'; + + assert(static_cast(p - iov.base) == len); + + return StringRef{iov.base, p}; +} + } // namespace http } // namespace shrpx diff --git a/src/shrpx_http.h b/src/shrpx_http.h index 37be7e16..7b9cbb17 100644 --- a/src/shrpx_http.h +++ b/src/shrpx_http.h @@ -79,6 +79,10 @@ StringRef create_affinity_cookie(BlockAllocator &balloc, const StringRef &name, bool require_cookie_secure_attribute(SessionAffinityCookieSecure secure, const StringRef &scheme); +// Returns RFC 7838 alt-svc header field value. +StringRef create_altsvc_header_value(BlockAllocator &balloc, + const std::vector &altsvcs); + } // namespace http } // namespace shrpx diff --git a/src/shrpx_http2_upstream.cc b/src/shrpx_http2_upstream.cc index 19d64e17..5f8b5254 100644 --- a/src/shrpx_http2_upstream.cc +++ b/src/shrpx_http2_upstream.cc @@ -1725,9 +1725,9 @@ int Http2Upstream::on_downstream_header_complete(Downstream *downstream) { } auto nva = std::vector(); - // 5 means :status and possible server, via, x-http2-push, and - // set-cookie (for affinity cookie) header field. - nva.reserve(resp.fs.headers().size() + 5 + + // 6 means :status and possible server, via, x-http2-push, alt-svc, + // and set-cookie (for affinity cookie) header field. + nva.reserve(resp.fs.headers().size() + 6 + httpconf.add_response_headers.size()); if (downstream->get_non_final_response()) { @@ -1795,6 +1795,14 @@ int Http2Upstream::on_downstream_header_complete(Downstream *downstream) { } } + if (!resp.fs.header(http2::HD_ALT_SVC)) { + // We won't change or alter alt-svc from backend for now + if (!httpconf.http2_altsvc_header_value.empty()) { + nva.push_back(http2::make_nv_ls_nocopy( + "alt-svc", httpconf.http2_altsvc_header_value)); + } + } + auto via = resp.fs.header(http2::HD_VIA); if (httpconf.no_via) { if (via) { diff --git a/src/shrpx_http_test.cc b/src/shrpx_http_test.cc index ba3fad97..7a9c63ab 100644 --- a/src/shrpx_http_test.cc +++ b/src/shrpx_http_test.cc @@ -118,4 +118,40 @@ void test_shrpx_http_create_affinity_cookie(void) { CU_ASSERT("charlie=01111111; Path=bar; Secure" == c); } +void test_shrpx_http_create_altsvc_header_value(void) { + { + BlockAllocator balloc(1024, 1024); + std::vector altsvcs{ + AltSvc{ + .protocol_id = StringRef::from_lit("h3"), + .host = StringRef::from_lit("127.0.0.1"), + .service = StringRef::from_lit("443"), + .params = StringRef::from_lit("ma=3600"), + }, + }; + + CU_ASSERT(R"(h3="127.0.0.1:443"; ma=3600)" == + http::create_altsvc_header_value(balloc, altsvcs)); + } + + { + BlockAllocator balloc(1024, 1024); + std::vector altsvcs{ + AltSvc{ + .protocol_id = StringRef::from_lit("h3"), + .service = StringRef::from_lit("443"), + .params = StringRef::from_lit("ma=3600"), + }, + AltSvc{ + .protocol_id = StringRef::from_lit("h3%"), + .host = StringRef::from_lit("\"foo\""), + .service = StringRef::from_lit("4433"), + }, + }; + + CU_ASSERT(R"(h3=":443"; ma=3600, h3%25="\"foo\":4433")" == + http::create_altsvc_header_value(balloc, altsvcs)); + } +} + } // namespace shrpx diff --git a/src/shrpx_http_test.h b/src/shrpx_http_test.h index 8bd8395d..8c3b1d93 100644 --- a/src/shrpx_http_test.h +++ b/src/shrpx_http_test.h @@ -34,6 +34,7 @@ namespace shrpx { void test_shrpx_http_create_forwarded(void); void test_shrpx_http_create_via_header_value(void); void test_shrpx_http_create_affinity_cookie(void); +void test_shrpx_http_create_altsvc_header_value(void); } // namespace shrpx diff --git a/src/shrpx_https_upstream.cc b/src/shrpx_https_upstream.cc index 2b51af38..d9bef5e2 100644 --- a/src/shrpx_https_upstream.cc +++ b/src/shrpx_https_upstream.cc @@ -1055,18 +1055,6 @@ std::unique_ptr HttpsUpstream::pop_downstream() { return std::unique_ptr(downstream_.release()); } -namespace { -void write_altsvc(DefaultMemchunks *buf, BlockAllocator &balloc, - const AltSvc &altsvc) { - buf->append(util::percent_encode_token(balloc, altsvc.protocol_id)); - buf->append("=\""); - buf->append(util::quote_string(balloc, altsvc.host)); - buf->append(':'); - buf->append(altsvc.service); - buf->append('"'); -} -} // namespace - int HttpsUpstream::on_downstream_header_complete(Downstream *downstream) { if (LOG_ENABLED(INFO)) { if (downstream->get_non_final_response()) { @@ -1225,13 +1213,7 @@ int HttpsUpstream::on_downstream_header_complete(Downstream *downstream) { // We won't change or alter alt-svc from backend for now if (!httpconf.altsvcs.empty()) { buf->append("Alt-Svc: "); - - auto &altsvcs = httpconf.altsvcs; - write_altsvc(buf, downstream->get_block_allocator(), altsvcs[0]); - for (size_t i = 1; i < altsvcs.size(); ++i) { - buf->append(", "); - write_altsvc(buf, downstream->get_block_allocator(), altsvcs[i]); - } + buf->append(httpconf.altsvc_header_value); buf->append("\r\n"); } } diff --git a/src/util.cc b/src/util.cc index c6ff16ab..bab90d56 100644 --- a/src/util.cc +++ b/src/util.cc @@ -167,24 +167,29 @@ bool in_attr_char(char c) { StringRef percent_encode_token(BlockAllocator &balloc, const StringRef &target) { auto iov = make_byte_ref(balloc, target.size() * 3 + 1); - auto p = iov.base; + auto p = percent_encode_token(iov.base, target); + + *p = '\0'; + + return StringRef{iov.base, p}; +} + +size_t percent_encode_tokenlen(const StringRef &target) { + size_t n = 0; for (auto first = std::begin(target); first != std::end(target); ++first) { uint8_t c = *first; if (c != '%' && in_token(c)) { - *p++ = c; + ++n; continue; } - *p++ = '%'; - *p++ = UPPER_XDIGITS[c >> 4]; - *p++ = UPPER_XDIGITS[(c & 0x0f)]; + // percent-encoded character '%ff' + n += 3; } - *p = '\0'; - - return StringRef{iov.base, p}; + return n; } uint32_t hex_to_uint(char c) { @@ -208,21 +213,27 @@ StringRef quote_string(BlockAllocator &balloc, const StringRef &target) { } auto iov = make_byte_ref(balloc, target.size() + cnt + 1); - auto p = iov.base; + auto p = quote_string(iov.base, target); - for (auto c : target) { - if (c == '"') { - *p++ = '\\'; - *p++ = '"'; - } else { - *p++ = c; - } - } *p = '\0'; return StringRef{iov.base, p}; } +size_t quote_stringlen(const StringRef &target) { + size_t n = 0; + + for (auto c : target) { + if (c == '"') { + n += 2; + } else { + ++n; + } + } + + return n; +} + namespace { template Iterator cpydig(Iterator d, uint32_t n, size_t len) { @@ -871,6 +882,42 @@ std::vector split_str(const StringRef &s, char delim) { return list; } +std::vector split_str(const StringRef &s, char delim, size_t n) { + if (n == 0) { + return split_str(s, delim); + } + + if (n == 1) { + return {s}; + } + + size_t len = 1; + auto last = std::end(s); + StringRef::const_iterator d; + for (auto first = std::begin(s); + len < n && (d = std::find(first, last, delim)) != last; + ++len, first = d + 1) + ; + + auto list = std::vector(len); + + len = 0; + for (auto first = std::begin(s);; ++len) { + if (len == n - 1) { + list[len] = StringRef{first, last}; + break; + } + + auto stop = std::find(first, last, delim); + list[len] = StringRef{first, stop}; + if (stop == last) { + break; + } + first = stop + 1; + } + return list; +} + std::vector parse_config_str_list(const StringRef &s, char delim) { auto sublist = split_str(s, delim); auto res = std::vector(); diff --git a/src/util.h b/src/util.h index 78b51b6f..2470008e 100644 --- a/src/util.h +++ b/src/util.h @@ -74,6 +74,8 @@ constexpr size_t NGHTTP2_MAX_UINT64_DIGITS = str_size("18446744073709551615"); namespace util { +extern const char UPPER_XDIGITS[]; + inline bool is_alpha(const char c) { return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z'); } @@ -136,10 +138,52 @@ StringRef percent_decode(BlockAllocator &balloc, const StringRef &src); // Percent encode |target| if character is not in token or '%'. StringRef percent_encode_token(BlockAllocator &balloc, const StringRef &target); +template +OutputIt percent_encode_token(OutputIt it, const StringRef &target) { + for (auto first = std::begin(target); first != std::end(target); ++first) { + uint8_t c = *first; + + if (c != '%' && in_token(c)) { + *it++ = c; + continue; + } + + *it++ = '%'; + *it++ = UPPER_XDIGITS[c >> 4]; + *it++ = UPPER_XDIGITS[(c & 0x0f)]; + } + + return it; +} + +// Returns the number of bytes written by percent_encode_token with +// the same |target| parameter. The return value does not include a +// terminal NUL byte. +size_t percent_encode_tokenlen(const StringRef &target); + // Returns quotedString version of |target|. Currently, this function // just replace '"' with '\"'. StringRef quote_string(BlockAllocator &balloc, const StringRef &target); +template +OutputIt quote_string(OutputIt it, const StringRef &target) { + for (auto c : target) { + if (c == '"') { + *it++ = '\\'; + *it++ = '"'; + } else { + *it++ = c; + } + } + + return it; +} + +// Returns the number of bytes written by quote_string with the same +// |target| parameter. The return value does not include a terminal +// NUL byte. +size_t quote_stringlen(const StringRef &target); + std::string format_hex(const unsigned char *s, size_t len); template std::string format_hex(const unsigned char (&s)[N]) { @@ -443,8 +487,6 @@ template std::string utos_funit(T n) { return dtos(static_cast(n) / (1 << b)) + u; } -extern const char UPPER_XDIGITS[]; - template std::string utox(T n) { std::string res; if (n == 0) { @@ -564,6 +606,11 @@ std::vector parse_config_str_list(const StringRef &s, // treated as a part of substring. std::vector split_str(const StringRef &s, char delim); +// Behaves like split_str, but this variant splits at most |n| - 1 +// times and returns at most |n| sub-strings. If |n| is zero, it +// falls back to split_str. +std::vector split_str(const StringRef &s, char delim, size_t n); + // Writes given time |tp| in Common Log format (e.g., // 03/Jul/2014:00:19:38 +0900) in buffer pointed by |out|. The buffer // must be at least 27 bytes, including terminal NULL byte. Expected diff --git a/src/util_test.cc b/src/util_test.cc index 6ad313d2..dfe87e99 100644 --- a/src/util_test.cc +++ b/src/util_test.cc @@ -652,4 +652,44 @@ void test_util_split_hostport(void) { util::split_hostport(StringRef::from_lit("[::1]80"))); } +void test_util_split_str(void) { + CU_ASSERT(std::vector{StringRef::from_lit("")} == + util::split_str(StringRef::from_lit(""), ',')); + CU_ASSERT(std::vector{StringRef::from_lit("alpha")} == + util::split_str(StringRef::from_lit("alpha"), ',')); + CU_ASSERT((std::vector{StringRef::from_lit("alpha"), + StringRef::from_lit("")}) == + util::split_str(StringRef::from_lit("alpha,"), ',')); + CU_ASSERT((std::vector{StringRef::from_lit("alpha"), + StringRef::from_lit("bravo")}) == + util::split_str(StringRef::from_lit("alpha,bravo"), ',')); + CU_ASSERT((std::vector{StringRef::from_lit("alpha"), + StringRef::from_lit("bravo"), + StringRef::from_lit("charlie")}) == + util::split_str(StringRef::from_lit("alpha,bravo,charlie"), ',')); + CU_ASSERT( + (std::vector{StringRef::from_lit("alpha"), + StringRef::from_lit("bravo"), + StringRef::from_lit("charlie")}) == + util::split_str(StringRef::from_lit("alpha,bravo,charlie"), ',', 0)); + CU_ASSERT(std::vector{StringRef::from_lit("")} == + util::split_str(StringRef::from_lit(""), ',', 1)); + CU_ASSERT(std::vector{StringRef::from_lit("")} == + util::split_str(StringRef::from_lit(""), ',', 2)); + CU_ASSERT( + (std::vector{StringRef::from_lit("alpha"), + StringRef::from_lit("bravo,charlie")}) == + util::split_str(StringRef::from_lit("alpha,bravo,charlie"), ',', 2)); + CU_ASSERT(std::vector{StringRef::from_lit("alpha")} == + util::split_str(StringRef::from_lit("alpha"), ',', 2)); + CU_ASSERT((std::vector{StringRef::from_lit("alpha"), + StringRef::from_lit("")}) == + util::split_str(StringRef::from_lit("alpha,"), ',', 2)); + CU_ASSERT(std::vector{StringRef::from_lit("alpha")} == + util::split_str(StringRef::from_lit("alpha"), ',', 0)); + CU_ASSERT( + std::vector{StringRef::from_lit("alpha,bravo,charlie")} == + util::split_str(StringRef::from_lit("alpha,bravo,charlie"), ',', 1)); +} + } // namespace shrpx diff --git a/src/util_test.h b/src/util_test.h index cc34f7d2..463c74f6 100644 --- a/src/util_test.h +++ b/src/util_test.h @@ -68,6 +68,7 @@ void test_util_is_hex_string(void); void test_util_decode_hex(void); void test_util_extract_host(void); void test_util_split_hostport(void); +void test_util_split_str(void); } // namespace shrpx