nghttpx: Add --http2-altsvc option

This commit is contained in:
Tatsuhiro Tsujikawa 2021-08-26 20:59:32 +09:00
parent d88eadff13
commit 51bf79bb8c
15 changed files with 366 additions and 85 deletions

View File

@ -179,6 +179,7 @@ OPTIONS = [
"no-strip-incoming-early-data",
"bpf-program-file",
"no-bpf",
"http2-altsvc",
]
LOGVARS = [

View File

@ -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) ||

View File

@ -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=<PROTOID,PORT[,HOST,[ORIGIN]]>
--altsvc=<PROTOID,PORT[,HOST,[ORIGIN[,PARAMS]]]>
Specify protocol ID, port, host and origin of
alternative service. <HOST> and <ORIGIN> 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. <HOST>, <ORIGIN> and <PARAMS> are
optional. Empty <HOST> and <ORIGIN> 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=<PROTOID,PORT[,HOST,[ORIGIN[,PARAMS]]]>
Just like --altsvc option, but this altsvc is only sent
in HTTP/2 frontend.
--add-request-header=<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;
}

View File

@ -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<int>(std::numeric_limits<uint16_t>::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<int>(std::numeric_limits<uint16_t>::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";

View File

@ -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<AltSvc> altsvcs;
// altsvcs serialized in a wire format.
StringRef altsvc_header_value;
std::vector<AltSvc> http2_altsvcs;
// http2_altsvcs serialized in a wire format.
StringRef http2_altsvc_header_value;
std::vector<ErrorPage> 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,

View File

@ -220,6 +220,57 @@ bool require_cookie_secure_attribute(SessionAffinityCookieSecure secure,
}
}
StringRef create_altsvc_header_value(BlockAllocator &balloc,
const std::vector<AltSvc> &altsvcs) {
// <PROTOID>="<HOST>:<SERVICE>"; <PARAMS>
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<size_t>(p - iov.base) == len);
return StringRef{iov.base, p};
}
} // namespace http
} // namespace shrpx

View File

@ -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<AltSvc> &altsvcs);
} // namespace http
} // namespace shrpx

View File

@ -1725,9 +1725,9 @@ int Http2Upstream::on_downstream_header_complete(Downstream *downstream) {
}
auto nva = std::vector<nghttp2_nv>();
// 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) {

View File

@ -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<AltSvc> 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<AltSvc> 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

View File

@ -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

View File

@ -1055,18 +1055,6 @@ std::unique_ptr<Downstream> HttpsUpstream::pop_downstream() {
return std::unique_ptr<Downstream>(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");
}
}

View File

@ -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 <typename Iterator>
Iterator cpydig(Iterator d, uint32_t n, size_t len) {
@ -871,6 +882,42 @@ std::vector<StringRef> split_str(const StringRef &s, char delim) {
return list;
}
std::vector<StringRef> 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<StringRef>(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<std::string> parse_config_str_list(const StringRef &s, char delim) {
auto sublist = split_str(s, delim);
auto res = std::vector<std::string>();

View File

@ -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 <typename OutputIt>
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 <typename OutputIt>
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 <size_t N> std::string format_hex(const unsigned char (&s)[N]) {
@ -443,8 +487,6 @@ template <typename T> std::string utos_funit(T n) {
return dtos(static_cast<double>(n) / (1 << b)) + u;
}
extern const char UPPER_XDIGITS[];
template <typename T> std::string utox(T n) {
std::string res;
if (n == 0) {
@ -564,6 +606,11 @@ std::vector<std::string> parse_config_str_list(const StringRef &s,
// treated as a part of substring.
std::vector<StringRef> 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<StringRef> 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

View File

@ -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>{StringRef::from_lit("")} ==
util::split_str(StringRef::from_lit(""), ','));
CU_ASSERT(std::vector<StringRef>{StringRef::from_lit("alpha")} ==
util::split_str(StringRef::from_lit("alpha"), ','));
CU_ASSERT((std::vector<StringRef>{StringRef::from_lit("alpha"),
StringRef::from_lit("")}) ==
util::split_str(StringRef::from_lit("alpha,"), ','));
CU_ASSERT((std::vector<StringRef>{StringRef::from_lit("alpha"),
StringRef::from_lit("bravo")}) ==
util::split_str(StringRef::from_lit("alpha,bravo"), ','));
CU_ASSERT((std::vector<StringRef>{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>{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>{StringRef::from_lit("")} ==
util::split_str(StringRef::from_lit(""), ',', 1));
CU_ASSERT(std::vector<StringRef>{StringRef::from_lit("")} ==
util::split_str(StringRef::from_lit(""), ',', 2));
CU_ASSERT(
(std::vector<StringRef>{StringRef::from_lit("alpha"),
StringRef::from_lit("bravo,charlie")}) ==
util::split_str(StringRef::from_lit("alpha,bravo,charlie"), ',', 2));
CU_ASSERT(std::vector<StringRef>{StringRef::from_lit("alpha")} ==
util::split_str(StringRef::from_lit("alpha"), ',', 2));
CU_ASSERT((std::vector<StringRef>{StringRef::from_lit("alpha"),
StringRef::from_lit("")}) ==
util::split_str(StringRef::from_lit("alpha,"), ',', 2));
CU_ASSERT(std::vector<StringRef>{StringRef::from_lit("alpha")} ==
util::split_str(StringRef::from_lit("alpha"), ',', 0));
CU_ASSERT(
std::vector<StringRef>{StringRef::from_lit("alpha,bravo,charlie")} ==
util::split_str(StringRef::from_lit("alpha,bravo,charlie"), ',', 1));
}
} // namespace shrpx

View File

@ -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