Select backend based on request host and path by extending -b option

-b option syntax is now <HOST>,<PORT>[;<PATTERN>[:...]].  The optional
<PATTERN>s specify the request host and path it is used for.  The
<PATTERN> can contain path, host + path or host.  The matching rule is
closely designed to ServeMux in Go programming language.
This commit is contained in:
Tatsuhiro Tsujikawa 2015-07-10 02:52:11 +09:00
parent 1940413eb3
commit 3119fc259c
27 changed files with 613 additions and 123 deletions

View File

@ -37,6 +37,8 @@
#include "http-parser/http_parser.h"
#include "util.h"
namespace nghttp2 {
struct Header {
@ -298,6 +300,40 @@ int lookup_method_token(const std::string &name);
const char *to_method_string(int method_token);
template <typename InputIt>
std::string normalize_path(InputIt first, InputIt last) {
// First, decode %XX for unreserved characters, then do
// http2::join_path
std::string result;
// We won't find %XX if length is less than 3.
if (last - first < 3) {
result.assign(first, last);
} else {
for (; first < last - 2;) {
if (*first == '%') {
if (util::isHexDigit(*(first + 1)) && util::isHexDigit(*(first + 2))) {
auto c = (util::hex_to_uint(*(first + 1)) << 4) +
util::hex_to_uint(*(first + 2));
if (util::inRFC3986UnreservedChars(c)) {
result += c;
first += 3;
continue;
}
result += '%';
result += util::upcase(*(first + 1));
result += util::upcase(*(first + 2));
first += 3;
continue;
}
}
result += *first++;
}
result.append(first, last);
}
return path_join(nullptr, 0, nullptr, 0, result.c_str(), result.size(),
nullptr, 0);
}
} // namespace http2
} // namespace nghttp2

View File

@ -829,4 +829,31 @@ void test_http2_path_join(void) {
}
}
void test_http2_normalize_path(void) {
std::string src;
src = "/alpha/bravo/../charlie";
CU_ASSERT("/alpha/charlie" ==
http2::normalize_path(std::begin(src), std::end(src)));
src = "/a%6c%70%68%61";
CU_ASSERT("/alpha" == http2::normalize_path(std::begin(src), std::end(src)));
src = "/alpha%2f%3a";
CU_ASSERT("/alpha%2F%3A" ==
http2::normalize_path(std::begin(src), std::end(src)));
src = "%2f";
CU_ASSERT("/%2F" == http2::normalize_path(std::begin(src), std::end(src)));
src = "%f";
CU_ASSERT("/%f" == http2::normalize_path(std::begin(src), std::end(src)));
src = "%";
CU_ASSERT("/%" == http2::normalize_path(std::begin(src), std::end(src)));
src = "";
CU_ASSERT("/" == http2::normalize_path(std::begin(src), std::end(src)));
}
} // namespace shrpx

View File

@ -45,6 +45,7 @@ void test_http2_http2_header_allowed(void);
void test_http2_mandatory_request_headers_presence(void);
void test_http2_parse_link_header(void);
void test_http2_path_join(void);
void test_http2_normalize_path(void);
} // namespace shrpx

View File

@ -96,6 +96,8 @@ int main(int argc, char *argv[]) {
!CU_add_test(pSuite, "http2_parse_link_header",
shrpx::test_http2_parse_link_header) ||
!CU_add_test(pSuite, "http2_path_join", shrpx::test_http2_path_join) ||
!CU_add_test(pSuite, "http2_normalize_path",
shrpx::test_http2_normalize_path) ||
!CU_add_test(pSuite, "downstream_index_request_headers",
shrpx::test_downstream_index_request_headers) ||
!CU_add_test(pSuite, "downstream_index_response_headers",
@ -118,6 +120,8 @@ int main(int argc, char *argv[]) {
shrpx::test_shrpx_config_parse_log_format) ||
!CU_add_test(pSuite, "config_read_tls_ticket_key_file",
shrpx::test_shrpx_config_read_tls_ticket_key_file) ||
!CU_add_test(pSuite, "config_match_downstream_addr_group",
shrpx::test_shrpx_config_match_downstream_addr_group) ||
!CU_add_test(pSuite, "util_streq", shrpx::test_util_streq) ||
!CU_add_test(pSuite, "util_strieq", shrpx::test_util_strieq) ||
!CU_add_test(pSuite, "util_inp_strlower",

View File

@ -966,6 +966,7 @@ void fill_default_config() {
mod_config()->no_ocsp = false;
mod_config()->header_field_buffer = 64_k;
mod_config()->max_header_fields = 100;
mod_config()->downstream_addr_group_catch_all = 0;
}
} // namespace
@ -997,14 +998,45 @@ Options:
The options are categorized into several groups.
Connections:
-b, --backend=<HOST,PORT>
-b, --backend=<HOST>,<PORT>[;<PATTERN>[:...]]
Set backend host and port. The multiple backend
addresses are accepted by repeating this option. UNIX
domain socket can be specified by prefixing path name
with "unix:" (e.g., unix:/var/run/backend.sock)
with "unix:" (e.g., unix:/var/run/backend.sock).
Optionally, if <PATTERN>s are given, the backend address
is only used if request matches the pattern. If -s, -p,
--client or --http2-bridge is used, <PATTERN>s are
ignored. The pattern matching is closely designed to
ServeMux in net/http package of Go programming language.
<PATTERN> consists of path, host + path or host. The
path must starts with "/". If it ends with "/", it
matches to the request path whose prefix is the path.
If it does not end with "/", it performs exact match
against the request path. If host is given, it performs
exact match against the request host. If host alone is
given, "/" is appended to it, so that it matches all
paths under the host (e.g., specifying "nghttp2.org"
equals to "nghttp2.org/"). Longer patterns take
precedence over shorter ones, breaking a tie by the
order of the appearance in the configuration. If
<PATTERN> is omitted, "/" is used as pattern, which
matches all paths (catch-all pattern). For example,
-b'127.0.0.1,8080;nghttp2.org/httpbin/' matches the
request host "nghttp2.org" and the request path
"/httpbin/get", but does not match the request host
"nghttp2.org" and the request path "/index.html". The
multiple <PATTERN>s can be specified, delimiting them by
":". Specifying
-b'127.0.0.1,8080;nghttp2.org:www.nghttp2.org' has the
same effect to specify -b'127.0.0.1,8080;nghttp2.org'
and -b'127.0.0.1,8080:www.nghttp2.org'. The backend
addresses sharing same <PATTERN> are grouped together
forming load balancing group. Since ";" and ":" are
used as delimiter, <PATTERN> must not contain these
characters.
Default: )" << DEFAULT_DOWNSTREAM_HOST << ","
<< DEFAULT_DOWNSTREAM_PORT << R"(
-f, --frontend=<HOST,PORT>
-f, --frontend=<HOST>,<PORT>
Set frontend host and port. If <HOST> is '*', it
assumes all addresses including both IPv4 and IPv6.
UNIX domain socket can be specified by prefixing path
@ -2118,55 +2150,97 @@ int main(int argc, char **argv) {
}
}
if (get_config()->downstream_addrs.empty()) {
if (get_config()->downstream_addr_groups.empty()) {
DownstreamAddr addr;
addr.host = strcopy(DEFAULT_DOWNSTREAM_HOST);
addr.port = DEFAULT_DOWNSTREAM_PORT;
mod_config()->downstream_addrs.push_back(std::move(addr));
DownstreamAddrGroup g("/");
g.addrs.push_back(std::move(addr));
mod_config()->downstream_addr_groups.push_back(std::move(g));
} else if (get_config()->downstream_proto == PROTO_HTTP2 ||
get_config()->http2_proxy || get_config()->client_proxy) {
// We don't support host mapping in these cases. Move all
// non-catch-all patterns to catch-all pattern for HTTP/2 backend
DownstreamAddrGroup catch_all("/");
for (auto &g : mod_config()->downstream_addr_groups) {
std::move(std::begin(g.addrs), std::end(g.addrs),
std::back_inserter(catch_all.addrs));
}
std::vector<DownstreamAddrGroup>().swap(
mod_config()->downstream_addr_groups);
mod_config()->downstream_addr_groups.push_back(std::move(catch_all));
}
if (LOG_ENABLED(INFO)) {
LOG(INFO) << "Resolving backend address";
}
for (auto &addr : mod_config()->downstream_addrs) {
ssize_t catch_all_group = -1;
for (size_t i = 0; i < mod_config()->downstream_addr_groups.size(); ++i) {
auto &g = mod_config()->downstream_addr_groups[i];
if (g.pattern == "/") {
catch_all_group = i;
}
if (LOG_ENABLED(INFO)) {
LOG(INFO) << "Host-path pattern: group " << i << ": '" << g.pattern
<< "'";
for (auto &addr : g.addrs) {
LOG(INFO) << "group " << i << " -> " << addr.host.get()
<< (addr.host_unix ? "" : ":" + util::utos(addr.port));
}
}
}
if (addr.host_unix) {
// for AF_UNIX socket, we use "localhost" as host for backend
// hostport. This is used as Host header field to backend and
// not going to be passed to any syscalls.
addr.hostport =
strcopy(util::make_hostport("localhost", get_config()->port));
if (catch_all_group == -1) {
LOG(FATAL) << "-b: No catch-all backend address is configured";
exit(EXIT_FAILURE);
}
mod_config()->downstream_addr_group_catch_all = catch_all_group;
auto path = addr.host.get();
auto pathlen = strlen(path);
if (LOG_ENABLED(INFO)) {
LOG(INFO) << "Catch-all pattern is group " << catch_all_group;
}
if (pathlen + 1 > sizeof(addr.addr.un.sun_path)) {
LOG(FATAL) << "UNIX domain socket path " << path << " is too long > "
<< sizeof(addr.addr.un.sun_path);
exit(EXIT_FAILURE);
for (auto &g : mod_config()->downstream_addr_groups) {
for (auto &addr : g.addrs) {
if (addr.host_unix) {
// for AF_UNIX socket, we use "localhost" as host for backend
// hostport. This is used as Host header field to backend and
// not going to be passed to any syscalls.
addr.hostport =
strcopy(util::make_hostport("localhost", get_config()->port));
auto path = addr.host.get();
auto pathlen = strlen(path);
if (pathlen + 1 > sizeof(addr.addr.un.sun_path)) {
LOG(FATAL) << "UNIX domain socket path " << path << " is too long > "
<< sizeof(addr.addr.un.sun_path);
exit(EXIT_FAILURE);
}
LOG(INFO) << "Use UNIX domain socket path " << path
<< " for backend connection";
addr.addr.un.sun_family = AF_UNIX;
// copy path including terminal NULL
std::copy_n(path, pathlen + 1, addr.addr.un.sun_path);
addr.addrlen = sizeof(addr.addr.un);
continue;
}
LOG(INFO) << "Use UNIX domain socket path " << path
<< " for backend connection";
addr.hostport = strcopy(util::make_hostport(addr.host.get(), addr.port));
addr.addr.un.sun_family = AF_UNIX;
// copy path including terminal NULL
std::copy_n(path, pathlen + 1, addr.addr.un.sun_path);
addr.addrlen = sizeof(addr.addr.un);
continue;
}
addr.hostport = strcopy(util::make_hostport(addr.host.get(), addr.port));
if (resolve_hostname(
&addr.addr, &addr.addrlen, addr.host.get(), addr.port,
get_config()->backend_ipv4
? AF_INET
: (get_config()->backend_ipv6 ? AF_INET6 : AF_UNSPEC)) == -1) {
exit(EXIT_FAILURE);
if (resolve_hostname(
&addr.addr, &addr.addrlen, addr.host.get(), addr.port,
get_config()->backend_ipv4 ? AF_INET : (get_config()->backend_ipv6
? AF_INET6
: AF_UNSPEC)) == -1) {
exit(EXIT_FAILURE);
}
}
}
@ -2183,9 +2257,10 @@ int main(int argc, char **argv) {
}
}
if (get_config()->http2_downstream_connections_per_worker == 0) {
if (get_config()->downstream_proto == PROTO_HTTP2 &&
get_config()->http2_downstream_connections_per_worker == 0) {
mod_config()->http2_downstream_connections_per_worker =
get_config()->downstream_addrs.size();
get_config()->downstream_addr_groups[0].addrs.size();
}
if (get_config()->rlimit_nofile) {

View File

@ -588,7 +588,8 @@ void ClientHandler::pool_downstream_connection(
return;
}
if (LOG_ENABLED(INFO)) {
CLOG(INFO, this) << "Pooling downstream connection DCONN:" << dconn.get();
CLOG(INFO, this) << "Pooling downstream connection DCONN:" << dconn.get()
<< " in group " << dconn->get_group();
}
dconn->set_client_handler(nullptr);
auto dconn_pool = worker_->get_dconn_pool();
@ -605,9 +606,44 @@ void ClientHandler::remove_downstream_connection(DownstreamConnection *dconn) {
}
std::unique_ptr<DownstreamConnection>
ClientHandler::get_downstream_connection() {
ClientHandler::get_downstream_connection(Downstream *downstream) {
size_t group;
auto &groups = get_config()->downstream_addr_groups;
auto catch_all = get_config()->downstream_addr_group_catch_all;
// Fast path. If we have one group, it must be catch-all group.
// HTTP/2 and client proxy modes fall in this case. Currently,
// HTTP/2 backend does not perform host-path mapping.
if (groups.size() == 1 || get_config()->downstream_proto == PROTO_HTTP2) {
group = 0;
} else if (downstream->get_request_method() == HTTP_CONNECT) {
// We don't know how to treat CONNECT request in host-path
// mapping. It most likely appears in proxy scenario. Since we
// have dealt with proxy case already, just use catch-all group.
group = catch_all;
} else {
if (!downstream->get_request_http2_authority().empty()) {
group = match_downstream_addr_group(
downstream->get_request_http2_authority(),
downstream->get_request_path(), groups, catch_all);
} else {
auto h = downstream->get_request_header(http2::HD_HOST);
if (h) {
group = match_downstream_addr_group(
h->value, downstream->get_request_path(), groups, catch_all);
} else {
group = match_downstream_addr_group("", downstream->get_request_path(),
groups, catch_all);
}
}
}
if (LOG_ENABLED(INFO)) {
CLOG(INFO, this) << "Downstream address group: " << group;
}
auto dconn_pool = worker_->get_dconn_pool();
auto dconn = dconn_pool->pop_downstream_connection();
auto dconn = dconn_pool->pop_downstream_connection(group);
if (!dconn) {
if (LOG_ENABLED(INFO)) {
@ -620,7 +656,8 @@ ClientHandler::get_downstream_connection() {
if (http2session_) {
dconn = make_unique<Http2DownstreamConnection>(dconn_pool, http2session_);
} else {
dconn = make_unique<HttpDownstreamConnection>(dconn_pool, conn_.loop);
dconn =
make_unique<HttpDownstreamConnection>(dconn_pool, group, conn_.loop);
}
dconn->set_client_handler(this);
return dconn;

View File

@ -92,7 +92,8 @@ public:
void pool_downstream_connection(std::unique_ptr<DownstreamConnection> dconn);
void remove_downstream_connection(DownstreamConnection *dconn);
std::unique_ptr<DownstreamConnection> get_downstream_connection();
std::unique_ptr<DownstreamConnection>
get_downstream_connection(Downstream *downstream);
MemchunkPool *get_mcpool();
SSL *get_ssl() const;
ConnectBlocker *get_connect_blocker() const;

View File

@ -57,6 +57,7 @@
#include "http2.h"
#include "util.h"
#include "template.h"
#include "base64.h"
using namespace nghttp2;
@ -79,9 +80,29 @@ TicketKeys::~TicketKeys() {
}
}
DownstreamAddr::DownstreamAddr(const DownstreamAddr &other)
: addr(other.addr), host(other.host ? strcopy(other.host.get()) : nullptr),
hostport(other.hostport ? strcopy(other.hostport.get()) : nullptr),
addrlen(other.addrlen), port(other.port), host_unix(other.host_unix) {}
DownstreamAddr &DownstreamAddr::operator=(const DownstreamAddr &other) {
if (this == &other) {
return *this;
}
addr = other.addr;
host = (other.host ? strcopy(other.host.get()) : nullptr);
hostport = (other.hostport ? strcopy(other.hostport.get()) : nullptr);
addrlen = other.addrlen;
port = other.port;
host_unix = other.host_unix;
return *this;
}
namespace {
int split_host_port(char *host, size_t hostlen, uint16_t *port_ptr,
const char *hostport) {
const char *hostport, size_t hostportlen) {
// host and port in |hostport| is separated by single ','.
const char *p = strchr(hostport, ',');
if (!p) {
@ -97,12 +118,13 @@ int split_host_port(char *host, size_t hostlen, uint16_t *port_ptr,
host[len] = '\0';
errno = 0;
unsigned long d = strtoul(p + 1, nullptr, 10);
if (errno == 0 && 1 <= d && d <= std::numeric_limits<uint16_t>::max()) {
auto portlen = hostportlen - len - 1;
auto d = util::parse_uint(reinterpret_cast<const uint8_t *>(p + 1), portlen);
if (1 <= d && d <= std::numeric_limits<uint16_t>::max()) {
*port_ptr = d;
return 0;
} else {
LOG(ERROR) << "Port is invalid: " << p + 1;
LOG(ERROR) << "Port is invalid: " << std::string(p + 1, portlen);
return -1;
}
}
@ -220,16 +242,16 @@ std::unique_ptr<char[]> strcopy(const std::string &val) {
return strcopy(val.c_str(), val.size());
}
std::vector<char *> parse_config_str_list(const char *s) {
std::vector<char *> parse_config_str_list(const char *s, char delim) {
size_t len = 1;
for (const char *first = s, *p = nullptr; (p = strchr(first, ','));
for (const char *first = s, *p = nullptr; (p = strchr(first, delim));
++len, first = p + 1)
;
auto list = std::vector<char *>(len);
auto first = strdup(s);
len = 0;
for (;;) {
auto p = strchr(first, ',');
auto p = strchr(first, delim);
if (p == nullptr) {
break;
}
@ -440,30 +462,82 @@ 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(DownstreamAddr addr, const char *src) {
// 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 = parse_config_str_list(src, ':');
assert(!mapping.empty());
for (auto raw_pattern : mapping) {
auto done = false;
std::string pattern;
auto slash = strchr(raw_pattern, '/');
if (slash == nullptr) {
// This effectively makes empty pattern to "/".
pattern = raw_pattern;
util::inp_strlower(pattern);
pattern += "/";
} else {
pattern.assign(raw_pattern, slash);
util::inp_strlower(pattern);
pattern +=
http2::normalize_path(slash, raw_pattern + strlen(raw_pattern));
}
for (auto &g : mod_config()->downstream_addr_groups) {
if (g.pattern == pattern) {
g.addrs.push_back(addr);
done = true;
break;
}
}
if (done) {
continue;
}
DownstreamAddrGroup g(pattern);
g.addrs.push_back(addr);
mod_config()->downstream_addr_groups.push_back(std::move(g));
}
clear_config_str_list(mapping);
}
} // namespace
int parse_config(const char *opt, const char *optarg) {
char host[NI_MAXHOST];
uint16_t port;
if (util::strieq(opt, SHRPX_OPT_BACKEND)) {
auto optarglen = strlen(optarg);
auto pat_delim = strchr(optarg, ';');
if (!pat_delim) {
pat_delim = optarg + optarglen;
}
DownstreamAddr addr;
if (util::istartsWith(optarg, SHRPX_UNIX_PATH_PREFIX)) {
DownstreamAddr addr;
auto path = optarg + str_size(SHRPX_UNIX_PATH_PREFIX);
addr.host = strcopy(path);
addr.host_unix = true;
} else {
if (split_host_port(host, sizeof(host), &port, optarg,
pat_delim - optarg) == -1) {
return -1;
}
mod_config()->downstream_addrs.push_back(std::move(addr));
return 0;
addr.host = strcopy(host);
addr.port = port;
}
if (split_host_port(host, sizeof(host), &port, optarg) == -1) {
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";
return -1;
}
DownstreamAddr addr;
addr.host = strcopy(host);
addr.port = port;
mod_config()->downstream_addrs.push_back(std::move(addr));
parse_mapping(std::move(addr), mapping);
return 0;
}
@ -478,7 +552,8 @@ int parse_config(const char *opt, const char *optarg) {
return 0;
}
if (split_host_port(host, sizeof(host), &port, optarg) == -1) {
if (split_host_port(host, sizeof(host), &port, optarg, strlen(optarg)) ==
-1) {
return -1;
}
@ -1322,4 +1397,117 @@ int int_syslog_facility(const char *strfacility) {
return -1;
}
namespace {
bool path_match(const std::string &pattern, const std::string &path) {
if (pattern.back() != '/') {
return pattern == path;
}
return util::startsWith(path, pattern);
}
} // namespace
namespace {
ssize_t match(const std::string &path,
const std::vector<DownstreamAddrGroup> &groups) {
ssize_t res = -1;
size_t best = 0;
for (size_t i = 0; i < groups.size(); ++i) {
auto &g = groups[i];
auto &pattern = g.pattern;
if (!path_match(pattern, path)) {
continue;
}
if (res == -1 || best < pattern.size()) {
best = pattern.size();
res = i;
}
}
return res;
}
} // namespace
namespace {
size_t match_downstream_addr_group_host(
const std::string &host, const std::string &raw_path,
const std::vector<DownstreamAddrGroup> &groups, size_t catch_all) {
if (raw_path == "*") {
auto group = match(host + "/", groups);
if (group != -1) {
if (LOG_ENABLED(INFO)) {
LOG(INFO) << "Found pattern with query " << host
<< ", matched pattern=" << groups[group].pattern;
}
return group;
}
return catch_all;
}
// probably, not necessary most of the case, but just in case.
auto fragment = std::find(std::begin(raw_path), std::end(raw_path), '#');
auto query = std::find(std::begin(raw_path), fragment, '?');
auto path = http2::normalize_path(std::begin(raw_path), query);
if (LOG_ENABLED(INFO)) {
LOG(INFO) << "Perform mapping selection, using host=" << host
<< ", path=" << path;
}
auto group = match(host + path, groups);
if (group != -1) {
if (LOG_ENABLED(INFO)) {
LOG(INFO) << "Found pattern with query " << host + path
<< ", matched pattern=" << groups[group].pattern;
}
return group;
}
group = match(path, groups);
if (group != -1) {
if (LOG_ENABLED(INFO)) {
LOG(INFO) << "Found pattern with query " << path
<< ", matched pattern=" << groups[group].pattern;
}
return group;
}
if (LOG_ENABLED(INFO)) {
LOG(INFO) << "None match. Use catch-all pattern";
}
return catch_all;
}
} // namespace
size_t match_downstream_addr_group(
const std::string &hostport, const std::string &raw_path,
const std::vector<DownstreamAddrGroup> &groups, size_t catch_all) {
if (hostport.empty() ||
std::find(std::begin(hostport), std::end(hostport), '/') !=
std::end(hostport)) {
// We use '/' specially, and if '/' is included in host, it breaks
// our code. Select catch-all case.
return catch_all;
}
std::string host;
if (hostport[0] == '[') {
// assume this is IPv6 numeric address
auto p = std::find(std::begin(hostport), std::end(hostport), ']');
if (p == std::end(hostport)) {
return catch_all;
}
if (p + 1 < std::end(hostport) && *(p + 1) != ':') {
return catch_all;
}
host.assign(std::begin(hostport), p + 1);
} else {
auto p = std::find(std::begin(hostport), std::end(hostport), ':');
if (p == std::begin(hostport)) {
return catch_all;
}
host.assign(std::begin(hostport), p);
}
util::inp_strlower(host);
return match_downstream_addr_group_host(host, raw_path, groups, catch_all);
}
} // namespace shrpx

View File

@ -194,6 +194,11 @@ struct AltSvc {
struct DownstreamAddr {
DownstreamAddr() : addr{{0}}, addrlen(0), port(0), host_unix(false) {}
DownstreamAddr(const DownstreamAddr &other);
DownstreamAddr(DownstreamAddr &&) = default;
DownstreamAddr &operator=(const DownstreamAddr &other);
DownstreamAddr &operator=(DownstreamAddr &&other) = default;
sockaddr_union addr;
// backend address. If |host_unix| is true, this is UNIX domain
// socket path.
@ -206,6 +211,12 @@ struct DownstreamAddr {
bool host_unix;
};
struct DownstreamAddrGroup {
DownstreamAddrGroup(std::string pattern) : pattern(std::move(pattern)) {}
std::string pattern;
std::vector<DownstreamAddr> addrs;
};
struct TicketKey {
uint8_t name[16];
uint8_t aes_key[16];
@ -225,7 +236,7 @@ struct Config {
std::vector<std::pair<std::string, std::string>> add_response_headers;
std::vector<unsigned char> alpn_prefs;
std::vector<LogFragment> accesslog_format;
std::vector<DownstreamAddr> downstream_addrs;
std::vector<DownstreamAddrGroup> downstream_addr_groups;
std::vector<std::string> tls_ticket_key_files;
// binary form of http proxy host and port
sockaddr_union downstream_http_proxy_addr;
@ -311,6 +322,8 @@ struct Config {
size_t downstream_response_buffer_size;
size_t header_field_buffer;
size_t max_header_fields;
// The index of catch-all group in downstream_addr_groups.
size_t downstream_addr_group_catch_all;
// Bit mask to disable SSL/TLS protocol versions. This will be
// passed to SSL_CTX_set_options().
long int tls_proto_mask;
@ -376,15 +389,14 @@ int load_config(const char *filename);
// Read passwd from |filename|
std::string read_passwd_from_file(const char *filename);
// Parses comma delimited strings in |s| and returns the array of
// pointers, each element points to the each substring in |s|. The
// |s| must be comma delimited list of strings. The strings must be
// delimited by a single comma and any white spaces around it are
// treated as a part of protocol strings. This function may modify
// |s| and the caller must leave it as is after this call. This
// Parses delimited strings in |s| and returns the array of pointers,
// each element points to the each substring in |s|. The delimiter is
// given by |delim. The |s| must be comma delimited list of strings.
// The strings must be delimited by a single comma and any white
// spaces around it are treated as a part of protocol strings. This
// function copies |s| and first element in the return value points to
// it. It is caller's responsibility to deallocate its memory.
std::vector<char *> parse_config_str_list(const char *s);
std::vector<char *> parse_config_str_list(const char *s, char delim = ',');
// Clears all elements of |list|, which is returned by
// parse_config_str_list(). If list is not empty, list[0] is freed by
@ -423,6 +435,16 @@ FILE *open_file_for_write(const char *filename);
std::unique_ptr<TicketKeys>
read_tls_ticket_key_file(const std::vector<std::string> &files);
// Selects group based on request's |hostport| and |path|. |hostport|
// is the value taken from :authority or host header field, and may
// contain port. The |path| may contain query part. We require the
// catch-all pattern in place, so this function always selects one
// group. The catch-all group index is given in |catch_all|. All
// patterns are given in |groups|.
size_t match_downstream_addr_group(
const std::string &hostport, const std::string &path,
const std::vector<DownstreamAddrGroup> &groups, size_t catch_all);
} // namespace shrpx
#endif // SHRPX_CONFIG_H

View File

@ -48,7 +48,7 @@ void test_shrpx_config_parse_config_str_list(void) {
CU_ASSERT(0 == strcmp("", res[1]));
clear_config_str_list(res);
res = parse_config_str_list(",a,,");
res = parse_config_str_list(":a::", ':');
CU_ASSERT(4 == res.size());
CU_ASSERT(0 == strcmp("", res[0]));
CU_ASSERT(0 == strcmp("a", res[1]));
@ -172,4 +172,68 @@ void test_shrpx_config_read_tls_ticket_key_file(void) {
memcmp("a..............b", key->hmac_key, sizeof(key->hmac_key)));
}
void test_shrpx_config_match_downstream_addr_group(void) {
auto groups = std::vector<DownstreamAddrGroup>{
{"nghttp2.org/"},
{"nghttp2.org/alpha/bravo/"},
{"nghttp2.org/alpha/charlie"},
{"nghttp2.org/delta%3A"},
{"www.nghttp2.org/"},
{"[::1]/"},
};
CU_ASSERT(0 == match_downstream_addr_group("nghttp2.org", "/", groups, 255));
// port is removed
CU_ASSERT(0 ==
match_downstream_addr_group("nghttp2.org:8080", "/", groups, 255));
// host is case-insensitive
CU_ASSERT(4 == match_downstream_addr_group("WWW.nghttp2.org", "/alpha",
groups, 255));
// path part is case-sensitive
CU_ASSERT(0 == match_downstream_addr_group("nghttp2.org", "/Alpha/bravo",
groups, 255));
// unreserved characters are decoded before matching
CU_ASSERT(1 == match_downstream_addr_group("nghttp2.org", "/alpha/%62ravo/",
groups, 255));
CU_ASSERT(1 == match_downstream_addr_group(
"nghttp2.org", "/alpha/%62ravo/charlie", groups, 255));
CU_ASSERT(2 == match_downstream_addr_group("nghttp2.org", "/alpha/charlie",
groups, 255));
// pattern which does not end with '/' must match its entirely. So
// this matches to group 0, not group 2.
CU_ASSERT(0 == match_downstream_addr_group("nghttp2.org", "/alpha/charlie/",
groups, 255));
// percent-encoding is normalized to upper case hex digits.
CU_ASSERT(3 == match_downstream_addr_group("nghttp2.org", "/delta%3a", groups,
255));
// path component is normalized before mathcing
CU_ASSERT(1 == match_downstream_addr_group(
"nghttp2.org", "/alpha/charlie/%2e././bravo/delta/..",
groups, 255));
CU_ASSERT(255 ==
match_downstream_addr_group("example.org", "/", groups, 255));
CU_ASSERT(255 == match_downstream_addr_group("", "/", groups, 255));
CU_ASSERT(255 == match_downstream_addr_group("foo/bar", "/", groups, 255));
// If path is "*", only match with host + "/".
CU_ASSERT(0 == match_downstream_addr_group("nghttp2.org", "*", groups, 255));
CU_ASSERT(5 == match_downstream_addr_group("[::1]", "/", groups, 255));
CU_ASSERT(5 == match_downstream_addr_group("[::1]:8080", "/", groups, 255));
CU_ASSERT(255 == match_downstream_addr_group("[::1", "/", groups, 255));
CU_ASSERT(255 == match_downstream_addr_group("[::1]8000", "/", groups, 255));
}
} // namespace shrpx

View File

@ -35,6 +35,7 @@ void test_shrpx_config_parse_config_str_list(void);
void test_shrpx_config_parse_header(void);
void test_shrpx_config_parse_log_format(void);
void test_shrpx_config_read_tls_ticket_key_file(void);
void test_shrpx_config_match_downstream_addr_group(void);
} // namespace shrpx

View File

@ -57,6 +57,7 @@ public:
virtual void on_upstream_change(Upstream *uptream) = 0;
virtual int on_priority_change(int32_t pri) = 0;
virtual size_t get_group() const = 0;
// true if this object is poolable.
virtual bool poolable() const = 0;

View File

@ -27,33 +27,42 @@
namespace shrpx {
DownstreamConnectionPool::DownstreamConnectionPool() {}
DownstreamConnectionPool::DownstreamConnectionPool(size_t num_groups)
: gpool_(num_groups) {}
DownstreamConnectionPool::~DownstreamConnectionPool() {
for (auto dconn : pool_) {
delete dconn;
for (auto &pool : gpool_) {
for (auto dconn : pool) {
delete dconn;
}
}
}
void DownstreamConnectionPool::add_downstream_connection(
std::unique_ptr<DownstreamConnection> dconn) {
pool_.insert(dconn.release());
auto group = dconn->get_group();
assert(gpool_.size() > group);
gpool_[group].insert(dconn.release());
}
std::unique_ptr<DownstreamConnection>
DownstreamConnectionPool::pop_downstream_connection() {
if (pool_.empty()) {
DownstreamConnectionPool::pop_downstream_connection(size_t group) {
assert(gpool_.size() > group);
auto &pool = gpool_[group];
if (pool.empty()) {
return nullptr;
}
auto dconn = std::unique_ptr<DownstreamConnection>(*std::begin(pool_));
pool_.erase(std::begin(pool_));
auto dconn = std::unique_ptr<DownstreamConnection>(*std::begin(pool));
pool.erase(std::begin(pool));
return dconn;
}
void DownstreamConnectionPool::remove_downstream_connection(
DownstreamConnection *dconn) {
pool_.erase(dconn);
auto group = dconn->get_group();
assert(gpool_.size() > group);
gpool_[group].erase(dconn);
delete dconn;
}

View File

@ -36,15 +36,15 @@ class DownstreamConnection;
class DownstreamConnectionPool {
public:
DownstreamConnectionPool();
DownstreamConnectionPool(size_t num_groups);
~DownstreamConnectionPool();
void add_downstream_connection(std::unique_ptr<DownstreamConnection> dconn);
std::unique_ptr<DownstreamConnection> pop_downstream_connection();
std::unique_ptr<DownstreamConnection> pop_downstream_connection(size_t group);
void remove_downstream_connection(DownstreamConnection *dconn);
private:
std::set<DownstreamConnection *> pool_;
std::vector<std::set<DownstreamConnection *>> gpool_;
};
} // namespace shrpx

View File

@ -264,7 +264,7 @@ int Http2DownstreamConnection::push_request_headers() {
// addr_idx here.
auto addr_idx = http2session_->get_addr_idx();
auto downstream_hostport =
get_config()->downstream_addrs[addr_idx].hostport.get();
get_config()->downstream_addr_groups[0].addrs[addr_idx].hostport.get();
const char *authority = nullptr, *host = nullptr;
if (!no_host_rewrite) {

View File

@ -61,6 +61,8 @@ public:
virtual void on_upstream_change(Upstream *upstream) {}
virtual int on_priority_change(int32_t pri);
// Currently, HTTP/2 backend does not perform host-path mapping.
virtual size_t get_group() const { return 0; }
// This object is not poolable because we dont' have facility to
// migrate to another Http2Session object.

View File

@ -233,11 +233,16 @@ int Http2Session::disconnect(bool hard) {
return 0;
}
int Http2Session::check_cert() { return ssl::check_cert(conn_.tls.ssl); }
int Http2Session::check_cert() {
return ssl::check_cert(
conn_.tls.ssl, &get_config()->downstream_addr_groups[0].addrs[addr_idx_]);
}
int Http2Session::initiate_connection() {
int rv = 0;
auto &addrs = get_config()->downstream_addr_groups[0].addrs;
if (state_ == DISCONNECTED) {
if (connect_blocker_->blocked()) {
if (LOG_ENABLED(INFO)) {
@ -248,17 +253,19 @@ int Http2Session::initiate_connection() {
}
auto worker_stat = worker_->get_worker_stat();
addr_idx_ = worker_stat->next_downstream;
++worker_stat->next_downstream;
worker_stat->next_downstream %= get_config()->downstream_addrs.size();
auto &next_downstream = worker_stat->next_downstream[0];
addr_idx_ = next_downstream;
if (++next_downstream >= addrs.size()) {
next_downstream = 0;
}
if (LOG_ENABLED(INFO)) {
SSLOG(INFO, this) << "Using downstream address idx=" << addr_idx_
<< " out of " << get_config()->downstream_addrs.size();
<< " out of " << addrs.size();
}
}
auto &downstream_addr = get_config()->downstream_addrs[addr_idx_];
auto &downstream_addr = addrs[addr_idx_];
if (get_config()->downstream_http_proxy_host && state_ == DISCONNECTED) {
if (LOG_ENABLED(INFO)) {
@ -503,7 +510,8 @@ int Http2Session::downstream_connect_proxy() {
if (LOG_ENABLED(INFO)) {
SSLOG(INFO, this) << "Connected to the proxy";
}
auto &downstream_addr = get_config()->downstream_addrs[addr_idx_];
auto &downstream_addr =
get_config()->downstream_addr_groups[0].addrs[addr_idx_];
std::string req = "CONNECT ";
req += downstream_addr.hostport.get();

View File

@ -334,7 +334,7 @@ void Http2Upstream::initiate_downstream(Downstream *downstream) {
int rv;
rv = downstream->attach_downstream_connection(
handler_->get_downstream_connection());
handler_->get_downstream_connection(downstream));
if (rv != 0) {
// downstream connection fails, send error page
if (error_reply(downstream, 503) != 0) {
@ -1476,7 +1476,7 @@ int Http2Upstream::on_downstream_reset(bool no_retry) {
// downstream connection.
rv = downstream->attach_downstream_connection(
handler_->get_downstream_connection());
handler_->get_downstream_connection(downstream));
if (rv != 0) {
goto fail;
}

View File

@ -109,12 +109,12 @@ void connectcb(struct ev_loop *loop, ev_io *w, int revents) {
} // namespace
HttpDownstreamConnection::HttpDownstreamConnection(
DownstreamConnectionPool *dconn_pool, struct ev_loop *loop)
DownstreamConnectionPool *dconn_pool, size_t group, struct ev_loop *loop)
: DownstreamConnection(dconn_pool),
conn_(loop, -1, nullptr, get_config()->downstream_write_timeout,
get_config()->downstream_read_timeout, 0, 0, 0, 0, connectcb,
readcb, timeoutcb, this),
ioctrl_(&conn_.rlimit), response_htp_{0}, addr_idx_(0),
ioctrl_(&conn_.rlimit), response_htp_{0}, group_(group), addr_idx_(0),
connected_(false) {}
HttpDownstreamConnection::~HttpDownstreamConnection() {
@ -143,14 +143,17 @@ int HttpDownstreamConnection::attach_downstream(Downstream *downstream) {
auto worker = client_handler_->get_worker();
auto worker_stat = worker->get_worker_stat();
auto end = worker_stat->next_downstream;
auto &next_downstream = worker_stat->next_downstream[group_];
auto end = next_downstream;
auto &addrs = get_config()->downstream_addr_groups[group_].addrs;
for (;;) {
auto i = worker_stat->next_downstream;
++worker_stat->next_downstream;
worker_stat->next_downstream %= get_config()->downstream_addrs.size();
auto &addr = addrs[next_downstream];
auto i = next_downstream;
if (++next_downstream >= addrs.size()) {
next_downstream = 0;
}
conn_.fd = util::create_nonblock_socket(
get_config()->downstream_addrs[i].addr.storage.ss_family);
conn_.fd = util::create_nonblock_socket(addr.addr.storage.ss_family);
if (conn_.fd == -1) {
auto error = errno;
@ -162,8 +165,7 @@ int HttpDownstreamConnection::attach_downstream(Downstream *downstream) {
}
int rv;
rv = connect(conn_.fd, &get_config()->downstream_addrs[i].addr.sa,
get_config()->downstream_addrs[i].addrlen);
rv = connect(conn_.fd, &addr.addr.sa, addr.addrlen);
if (rv != 0 && errno != EINPROGRESS) {
auto error = errno;
DCLOG(WARN, this) << "connect() failed; errno=" << error;
@ -172,7 +174,7 @@ int HttpDownstreamConnection::attach_downstream(Downstream *downstream) {
close(conn_.fd);
conn_.fd = -1;
if (end == worker_stat->next_downstream) {
if (end == next_downstream) {
return SHRPX_ERR_NETWORK;
}
@ -212,8 +214,10 @@ int HttpDownstreamConnection::attach_downstream(Downstream *downstream) {
int HttpDownstreamConnection::push_request_headers() {
const char *authority = nullptr, *host = nullptr;
auto downstream_hostport =
get_config()->downstream_addrs[addr_idx_].hostport.get();
auto downstream_hostport = get_config()
->downstream_addr_groups[group_]
.addrs[addr_idx_]
.hostport.get();
auto connect_method = downstream_->get_request_method() == HTTP_CONNECT;
if (!get_config()->no_host_rewrite && !get_config()->http2_proxy &&
@ -877,4 +881,6 @@ void HttpDownstreamConnection::on_upstream_change(Upstream *upstream) {}
void HttpDownstreamConnection::signal_write() { conn_.wlimit.startw(); }
size_t HttpDownstreamConnection::get_group() const { return group_; }
} // namespace shrpx

View File

@ -39,7 +39,7 @@ class DownstreamConnectionPool;
class HttpDownstreamConnection : public DownstreamConnection {
public:
HttpDownstreamConnection(DownstreamConnectionPool *dconn_pool,
HttpDownstreamConnection(DownstreamConnectionPool *dconn_pool, size_t group,
struct ev_loop *loop);
virtual ~HttpDownstreamConnection();
virtual int attach_downstream(Downstream *downstream);
@ -58,6 +58,7 @@ public:
virtual void on_upstream_change(Upstream *upstream);
virtual int on_priority_change(int32_t pri) { return 0; }
virtual size_t get_group() const;
virtual bool poolable() const { return true; }
@ -68,6 +69,7 @@ private:
Connection conn_;
IOControl ioctrl_;
http_parser response_htp_;
size_t group_;
// index of get_config()->downstream_addrs this object is using
size_t addr_idx_;
bool connected_;

View File

@ -297,7 +297,7 @@ int htp_hdrs_completecb(http_parser *htp) {
}
rv = downstream->attach_downstream_connection(
upstream->get_client_handler()->get_downstream_connection());
upstream->get_client_handler()->get_downstream_connection(downstream));
if (rv != 0) {
downstream->set_request_state(Downstream::CONNECT_FAIL);
@ -993,7 +993,7 @@ int HttpsUpstream::on_downstream_reset(bool no_retry) {
}
rv = downstream_->attach_downstream_connection(
handler_->get_downstream_connection());
handler_->get_downstream_connection(downstream_.get()));
if (rv != 0) {
goto fail;
}

View File

@ -271,7 +271,7 @@ void SpdyUpstream::start_downstream(Downstream *downstream) {
void SpdyUpstream::initiate_downstream(Downstream *downstream) {
int rv = downstream->attach_downstream_connection(
handler_->get_downstream_connection());
handler_->get_downstream_connection(downstream));
if (rv != 0) {
// If downstream connection fails, issue RST_STREAM.
rst_stream(downstream, SPDYLAY_INTERNAL_ERROR);
@ -1104,7 +1104,7 @@ int SpdyUpstream::on_downstream_reset(bool no_retry) {
// downstream connection.
rv = downstream->attach_downstream_connection(
handler_->get_downstream_connection());
handler_->get_downstream_connection(downstream));
if (rv != 0) {
goto fail;
}

View File

@ -743,7 +743,7 @@ void get_altnames(X509 *cert, std::vector<std::string> &dns_names,
}
}
int check_cert(SSL *ssl) {
int check_cert(SSL *ssl, const DownstreamAddr *addr) {
auto cert = SSL_get_peer_certificate(ssl);
if (!cert) {
LOG(ERROR) << "No certificate found";
@ -760,9 +760,7 @@ int check_cert(SSL *ssl) {
std::vector<std::string> dns_names;
std::vector<std::string> ip_addrs;
get_altnames(cert, dns_names, ip_addrs, common_name);
if (verify_hostname(get_config()->downstream_addrs[0].host.get(),
&get_config()->downstream_addrs[0].addr,
get_config()->downstream_addrs[0].addrlen, dns_names,
if (verify_hostname(addr->host.get(), &addr->addr, addr->addrlen, dns_names,
ip_addrs, common_name) != 0) {
LOG(ERROR) << "Certificate verification failed: hostname does not match";
return -1;

View File

@ -40,6 +40,7 @@ namespace shrpx {
class ClientHandler;
class Worker;
class DownstreamConnectionPool;
struct DownstreamAddr;
namespace ssl {
@ -68,7 +69,7 @@ ClientHandler *accept_connection(Worker *worker, int fd, sockaddr *addr,
// Check peer's certificate against first downstream address in
// Config::downstream_addrs. We only consider first downstream since
// we use this function for HTTP/2 downstream link only.
int check_cert(SSL *ssl);
int check_cert(SSL *ssl, const DownstreamAddr *addr);
// Retrieves DNS and IP address in subjectAltNames and commonName from
// the |cert|.

View File

@ -61,8 +61,11 @@ void mcpool_clear_cb(struct ev_loop *loop, ev_timer *w, int revents) {
Worker::Worker(struct ev_loop *loop, SSL_CTX *sv_ssl_ctx, SSL_CTX *cl_ssl_ctx,
ssl::CertLookupTree *cert_tree,
const std::shared_ptr<TicketKeys> &ticket_keys)
: next_http2session_(0), loop_(loop), sv_ssl_ctx_(sv_ssl_ctx),
cl_ssl_ctx_(cl_ssl_ctx), cert_tree_(cert_tree), ticket_keys_(ticket_keys),
: next_http2session_(0),
dconn_pool_(get_config()->downstream_addr_groups.size()),
worker_stat_(get_config()->downstream_addr_groups.size()), loop_(loop),
sv_ssl_ctx_(sv_ssl_ctx), cl_ssl_ctx_(cl_ssl_ctx), cert_tree_(cert_tree),
ticket_keys_(ticket_keys),
connect_blocker_(make_unique<ConnectBlocker>(loop_)),
graceful_shutdown_(false) {
ev_async_init(&w_, eventcb);

View File

@ -55,13 +55,13 @@ class CertLookupTree;
} // namespace ssl
struct WorkerStat {
WorkerStat() : num_connections(0), next_downstream(0) {}
WorkerStat(size_t num_groups)
: num_connections(0), next_downstream(num_groups) {}
size_t num_connections;
// Next downstream index in Config::downstream_addrs. For HTTP/2
// downstream connections, this is always 0. For HTTP/1, this is
// Next downstream index in Config::downstream_addr_groups. This is
// used as load balancing.
size_t next_downstream;
std::vector<size_t> next_downstream;
};
enum WorkerEventType {

View File

@ -386,9 +386,13 @@ bool streq_l(const char (&a)[N], InputIt b, size_t blen) {
bool strifind(const char *a, const char *b);
template <typename InputIt> void inp_strlower(InputIt first, InputIt last) {
std::transform(first, last, first, lowcase);
}
// Lowercase |s| in place.
inline void inp_strlower(std::string &s) {
std::transform(std::begin(s), std::end(s), std::begin(s), lowcase);
inp_strlower(std::begin(s), std::end(s));
}
// Returns string representation of |n| with 2 fractional digits.