Merge pull request #1036 from nghttp2/nghttpx-affinity-cookie

nghttpx: Cookie based session affinity
This commit is contained in:
Tatsuhiro Tsujikawa 2017-11-01 22:45:38 +09:00 committed by GitHub
commit 8c0ea56bb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 502 additions and 49 deletions

View File

@ -9,6 +9,7 @@ import (
"golang.org/x/net/websocket" "golang.org/x/net/websocket"
"io" "io"
"net/http" "net/http"
"regexp"
"syscall" "syscall"
"testing" "testing"
"time" "time"
@ -125,6 +126,54 @@ Content-Length: 0
// } // }
// } // }
// TestH1H1AffinityCookie tests that affinity cookie is sent back in
// cleartext http.
func TestH1H1AffinityCookie(t *testing.T) {
st := newServerTester([]string{"--affinity-cookie"}, t, noopHandler)
defer st.Close()
res, err := st.http1(requestParam{
name: "TestH1H1AffinityCookie",
})
if err != nil {
t.Fatalf("Error st.http1() = %v", err)
}
if got, want := res.status, 200; got != want {
t.Errorf("status = %v; want %v", got, want)
}
const pattern = `affinity=[0-9a-f]{8}; Path=/foo/bar`
validCookie := regexp.MustCompile(pattern)
if got := res.header.Get("Set-Cookie"); !validCookie.MatchString(got) {
t.Errorf("Set-Cookie: %v; want pattern %v", got, pattern)
}
}
// TestH1H1AffinityCookieTLS tests that affinity cookie is sent back
// in https.
func TestH1H1AffinityCookieTLS(t *testing.T) {
st := newServerTesterTLS([]string{"--alpn-h1", "--affinity-cookie"}, t, noopHandler)
defer st.Close()
res, err := st.http1(requestParam{
name: "TestH1H1AffinityCookieTLS",
})
if err != nil {
t.Fatalf("Error st.http1() = %v", err)
}
if got, want := res.status, 200; got != want {
t.Errorf("status = %v; want %v", got, want)
}
const pattern = `affinity=[0-9a-f]{8}; Path=/foo/bar; Secure`
validCookie := regexp.MustCompile(pattern)
if got := res.header.Get("Set-Cookie"); !validCookie.MatchString(got) {
t.Errorf("Set-Cookie: %v; want pattern %v", got, pattern)
}
}
// TestH1H1GracefulShutdown tests graceful shutdown. // TestH1H1GracefulShutdown tests graceful shutdown.
func TestH1H1GracefulShutdown(t *testing.T) { func TestH1H1GracefulShutdown(t *testing.T) {
st := newServerTester(nil, t, noopHandler) st := newServerTester(nil, t, noopHandler)

View File

@ -1705,6 +1705,55 @@ func TestH2H1Code204TE(t *testing.T) {
} }
} }
// TestH2H1AffinityCookie tests that affinity cookie is sent back in
// cleartext http.
func TestH2H1AffinityCookie(t *testing.T) {
st := newServerTester([]string{"--affinity-cookie"}, t, noopHandler)
defer st.Close()
res, err := st.http2(requestParam{
name: "TestH2H1AffinityCookie",
})
if err != nil {
t.Fatalf("Error st.http2() = %v", err)
}
if got, want := res.status, 200; got != want {
t.Errorf("status = %v; want %v", got, want)
}
const pattern = `affinity=[0-9a-f]{8}; Path=/foo/bar`
validCookie := regexp.MustCompile(pattern)
if got := res.header.Get("Set-Cookie"); !validCookie.MatchString(got) {
t.Errorf("Set-Cookie: %v; want pattern %v", got, pattern)
}
}
// TestH2H1AffinityCookieTLS tests that affinity cookie is sent back
// in https.
func TestH2H1AffinityCookieTLS(t *testing.T) {
st := newServerTesterTLS([]string{"--affinity-cookie"}, t, noopHandler)
defer st.Close()
res, err := st.http2(requestParam{
name: "TestH2H1AffinityCookieTLS",
scheme: "https",
})
if err != nil {
t.Fatalf("Error st.http2() = %v", err)
}
if got, want := res.status, 200; got != want {
t.Errorf("status = %v; want %v", got, want)
}
const pattern = `affinity=[0-9a-f]{8}; Path=/foo/bar; Secure`
validCookie := regexp.MustCompile(pattern)
if got := res.header.Get("Set-Cookie"); !validCookie.MatchString(got) {
t.Errorf("Set-Cookie: %v; want pattern %v", got, pattern)
}
}
// TestH2H1GracefulShutdown tests graceful shutdown. // TestH2H1GracefulShutdown tests graceful shutdown.
func TestH2H1GracefulShutdown(t *testing.T) { func TestH2H1GracefulShutdown(t *testing.T) {
st := newServerTester(nil, t, noopHandler) st := newServerTester(nil, t, noopHandler)

View File

@ -101,7 +101,7 @@ func newServerTesterInternal(src_args []string, t *testing.T, handler http.Handl
args := []string{} args := []string{}
var backendTLS, dns, externalDNS, acceptProxyProtocol, redirectIfNotTLS bool var backendTLS, dns, externalDNS, acceptProxyProtocol, redirectIfNotTLS, affinityCookie, alpnH1 bool
for _, k := range src_args { for _, k := range src_args {
switch k { switch k {
@ -116,6 +116,10 @@ func newServerTesterInternal(src_args []string, t *testing.T, handler http.Handl
acceptProxyProtocol = true acceptProxyProtocol = true
case "--redirect-if-not-tls": case "--redirect-if-not-tls":
redirectIfNotTLS = true redirectIfNotTLS = true
case "--affinity-cookie":
affinityCookie = true
case "--alpn-h1":
alpnH1 = true
default: default:
args = append(args, k) args = append(args, k)
} }
@ -168,6 +172,10 @@ func newServerTesterInternal(src_args []string, t *testing.T, handler http.Handl
b += ";redirect-if-not-tls" b += ";redirect-if-not-tls"
} }
if affinityCookie {
b += ";affinity=cookie;affinity-cookie-name=affinity;affinity-cookie-path=/foo/bar"
}
noTLS := ";no-tls" noTLS := ";no-tls"
if frontendTLS { if frontendTLS {
noTLS = "" noTLS = ""
@ -218,7 +226,11 @@ func newServerTesterInternal(src_args []string, t *testing.T, handler http.Handl
tlsConfig = clientConfig tlsConfig = clientConfig
} }
tlsConfig.InsecureSkipVerify = true tlsConfig.InsecureSkipVerify = true
if alpnH1 {
tlsConfig.NextProtos = []string{"http/1.1"}
} else {
tlsConfig.NextProtos = []string{"h2", "spdy/3.1"} tlsConfig.NextProtos = []string{"h2", "spdy/3.1"}
}
conn, err = tls.Dial("tcp", authority, tlsConfig) conn, err = tls.Dial("tcp", authority, tlsConfig)
} else { } else {
conn, err = net.Dial("tcp", authority) conn, err = net.Dial("tcp", authority)

View File

@ -117,6 +117,8 @@ int main(int argc, char *argv[]) {
shrpx::test_downstream_rewrite_location_response_header) || shrpx::test_downstream_rewrite_location_response_header) ||
!CU_add_test(pSuite, "downstream_supports_non_final_response", !CU_add_test(pSuite, "downstream_supports_non_final_response",
shrpx::test_downstream_supports_non_final_response) || shrpx::test_downstream_supports_non_final_response) ||
!CU_add_test(pSuite, "downstream_find_affinity_cookie",
shrpx::test_downstream_find_affinity_cookie) ||
!CU_add_test(pSuite, "config_parse_header", !CU_add_test(pSuite, "config_parse_header",
shrpx::test_shrpx_config_parse_header) || shrpx::test_shrpx_config_parse_header) ||
!CU_add_test(pSuite, "config_parse_log_format", !CU_add_test(pSuite, "config_parse_log_format",
@ -131,6 +133,8 @@ int main(int argc, char *argv[]) {
shrpx::test_shrpx_http_create_forwarded) || shrpx::test_shrpx_http_create_forwarded) ||
!CU_add_test(pSuite, "http_create_via_header_value", !CU_add_test(pSuite, "http_create_via_header_value",
shrpx::test_shrpx_http_create_via_header_value) || 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, "router_match", shrpx::test_shrpx_router_match) || !CU_add_test(pSuite, "router_match", shrpx::test_shrpx_router_match) ||
!CU_add_test(pSuite, "router_match_wildcard", !CU_add_test(pSuite, "router_match_wildcard",
shrpx::test_shrpx_router_match_wildcard) || shrpx::test_shrpx_router_match_wildcard) ||

View File

@ -1772,16 +1772,28 @@ Connections:
The session affinity is enabled using The session affinity is enabled using
"affinity=<METHOD>" parameter. If "ip" is given in "affinity=<METHOD>" parameter. If "ip" is given in
<METHOD>, client IP based session affinity is enabled. <METHOD>, client IP based session affinity is enabled.
If "none" is given in <METHOD>, session affinity is If "cookie" is given in <METHOD>, cookie based session
disabled, and this is the default. The session affinity affinity is enabled. If "none" is given in <METHOD>,
is enabled per <PATTERN>. If at least one backend has session affinity is disabled, and this is the default.
"affinity" parameter, and its <METHOD> is not "none", The session affinity is enabled per <PATTERN>. If at
session affinity is enabled for all backend servers least one backend has "affinity" parameter, and its
sharing the same <PATTERN>. It is advised to set <METHOD> is not "none", session affinity is enabled for
"affinity" parameter to all backend explicitly if all backend servers sharing the same <PATTERN>. It is
session affinity is desired. The session affinity may advised to set "affinity" parameter to all backend
break if one of the backend gets unreachable, or backend explicitly if session affinity is desired. The session
settings are reloaded or replaced by API. affinity may break if one of the backend gets
unreachable, or backend settings are reloaded or
replaced by API.
If "affinity=cookie" is used, the additional
configuration is required.
"affinity-cookie-name=<NAME>" must be used to specify a
name of cookie to use. Optionally,
"affinity-cookie-path=<PATH>" can be used to specify a
path which cookie is applied. The Secure attribute of a
cookie is determined by a request scheme. If a request
scheme is "https", then Secure attribute is added.
Otherwise, it is not added.
By default, name resolution of backend host name is done By default, name resolution of backend host name is done
at start up, or reloading configuration. If "dns" at start up, or reloading configuration. If "dns"

View File

@ -699,7 +699,7 @@ void ClientHandler::pool_downstream_connection(
auto &shared_addr = group->shared_addr; auto &shared_addr = group->shared_addr;
if (shared_addr->affinity == AFFINITY_NONE) { if (shared_addr->affinity.type == AFFINITY_NONE) {
auto &dconn_pool = group->shared_addr->dconn_pool; auto &dconn_pool = group->shared_addr->dconn_pool;
dconn_pool.add_downstream_connection(std::move(dconn)); dconn_pool.add_downstream_connection(std::move(dconn));
@ -947,6 +947,24 @@ uint32_t next_cycle(const WeightedPri &pri) {
} }
} // namespace } // namespace
uint32_t ClientHandler::get_affinity_cookie(Downstream *downstream,
const StringRef &cookie_name) {
auto h = downstream->find_affinity_cookie(cookie_name);
if (h) {
return h;
}
auto d = std::uniform_int_distribution<uint32_t>(
1, std::numeric_limits<uint32_t>::max());
auto rh = d(worker_->get_randgen());
h = util::hash32(StringRef{reinterpret_cast<uint8_t *>(&rh),
reinterpret_cast<uint8_t *>(&rh) + sizeof(rh)});
downstream->renew_affinity_cookie(h);
return h;
}
std::unique_ptr<DownstreamConnection> std::unique_ptr<DownstreamConnection>
ClientHandler::get_downstream_connection(int &err, Downstream *downstream) { ClientHandler::get_downstream_connection(int &err, Downstream *downstream) {
size_t group_idx; size_t group_idx;
@ -1012,16 +1030,27 @@ ClientHandler::get_downstream_connection(int &err, Downstream *downstream) {
auto &group = groups[group_idx]; auto &group = groups[group_idx];
auto &shared_addr = group->shared_addr; auto &shared_addr = group->shared_addr;
if (shared_addr->affinity == AFFINITY_IP) { if (shared_addr->affinity.type != AFFINITY_NONE) {
uint32_t hash;
switch (shared_addr->affinity.type) {
case AFFINITY_IP:
if (!affinity_hash_computed_) { if (!affinity_hash_computed_) {
affinity_hash_ = compute_affinity_from_ip(ipaddr_); affinity_hash_ = compute_affinity_from_ip(ipaddr_);
affinity_hash_computed_ = true; affinity_hash_computed_ = true;
} }
hash = affinity_hash_;
break;
case AFFINITY_COOKIE:
hash = get_affinity_cookie(downstream, shared_addr->affinity.cookie.name);
break;
default:
assert(0);
}
const auto &affinity_hash = shared_addr->affinity_hash; const auto &affinity_hash = shared_addr->affinity_hash;
auto it = std::lower_bound( auto it = std::lower_bound(
std::begin(affinity_hash), std::end(affinity_hash), affinity_hash_, std::begin(affinity_hash), std::end(affinity_hash), hash,
[](const AffinityHash &lhs, uint32_t rhs) { return lhs.hash < rhs; }); [](const AffinityHash &lhs, uint32_t rhs) { return lhs.hash < rhs; });
if (it == std::end(affinity_hash)) { if (it == std::end(affinity_hash)) {

View File

@ -153,6 +153,11 @@ public:
Http2Session *select_http2_session_with_affinity( Http2Session *select_http2_session_with_affinity(
const std::shared_ptr<DownstreamAddrGroup> &group, DownstreamAddr *addr); const std::shared_ptr<DownstreamAddrGroup> &group, DownstreamAddr *addr);
// Returns an affinity cookie value for |downstream|. |cookie_name|
// is used to inspect cookie header field in request header fields.
uint32_t get_affinity_cookie(Downstream *downstream,
const StringRef &cookie_name);
const UpstreamAddr *get_upstream_addr() const; const UpstreamAddr *get_upstream_addr() const;
void repeat_read_timer(); void repeat_read_timer();

View File

@ -789,10 +789,10 @@ int parse_upstream_params(UpstreamParams &out, const StringRef &src_params) {
struct DownstreamParams { struct DownstreamParams {
StringRef sni; StringRef sni;
AffinityConfig affinity;
size_t fall; size_t fall;
size_t rise; size_t rise;
shrpx_proto proto; shrpx_proto proto;
shrpx_session_affinity affinity;
bool tls; bool tls;
bool dns; bool dns;
bool redirect_if_not_tls; bool redirect_if_not_tls;
@ -862,13 +862,27 @@ int parse_downstream_params(DownstreamParams &out,
} else if (util::istarts_with_l(param, "affinity=")) { } else if (util::istarts_with_l(param, "affinity=")) {
auto valstr = StringRef{first + str_size("affinity="), end}; auto valstr = StringRef{first + str_size("affinity="), end};
if (util::strieq_l("none", valstr)) { if (util::strieq_l("none", valstr)) {
out.affinity = AFFINITY_NONE; out.affinity.type = AFFINITY_NONE;
} else if (util::strieq_l("ip", valstr)) { } else if (util::strieq_l("ip", valstr)) {
out.affinity = AFFINITY_IP; out.affinity.type = AFFINITY_IP;
} else if (util::strieq_l("cookie", valstr)) {
out.affinity.type = AFFINITY_COOKIE;
} else { } else {
LOG(ERROR) << "backend: affinity: value must be either none or ip"; LOG(ERROR)
<< "backend: affinity: value must be one of none, ip, and cookie";
return -1; return -1;
} }
} else if (util::istarts_with_l(param, "affinity-cookie-name=")) {
auto val = StringRef{first + str_size("affinity-cookie-name="), end};
if (val.empty()) {
LOG(ERROR)
<< "backend: affinity-cookie-name: non empty string is expected";
return -1;
}
out.affinity.cookie.name = val;
} else if (util::istarts_with_l(param, "affinity-cookie-path=")) {
out.affinity.cookie.path =
StringRef{first + str_size("affinity-cookie-path="), end};
} else if (util::strieq_l("dns", param)) { } else if (util::strieq_l("dns", param)) {
out.dns = true; out.dns = true;
} else if (util::strieq_l("redirect-if-not-tls", param)) { } else if (util::strieq_l("redirect-if-not-tls", param)) {
@ -918,6 +932,13 @@ int parse_mapping(Config *config, DownstreamAddrConfig &addr,
return -1; return -1;
} }
if (params.affinity.type == AFFINITY_COOKIE &&
params.affinity.cookie.name.empty()) {
LOG(ERROR) << "backend: affinity-cookie-name is mandatory if "
"affinity=cookie is specified";
return -1;
}
addr.fall = params.fall; addr.fall = params.fall;
addr.rise = params.rise; addr.rise = params.rise;
addr.proto = params.proto; addr.proto = params.proto;
@ -962,8 +983,24 @@ int parse_mapping(Config *config, DownstreamAddrConfig &addr,
if (g.pattern == pattern) { if (g.pattern == pattern) {
// Last value wins if we have multiple different affinity // Last value wins if we have multiple different affinity
// value under one group. // value under one group.
if (params.affinity != AFFINITY_NONE) { if (params.affinity.type != AFFINITY_NONE) {
g.affinity = params.affinity; if (g.affinity.type == AFFINITY_NONE) {
g.affinity.type = params.affinity.type;
if (params.affinity.type == AFFINITY_COOKIE) {
g.affinity.cookie.name = make_string_ref(
downstreamconf.balloc, params.affinity.cookie.name);
if (!params.affinity.cookie.path.empty()) {
g.affinity.cookie.path = make_string_ref(
downstreamconf.balloc, params.affinity.cookie.path);
}
}
} else if (g.affinity.type != params.affinity.type ||
g.affinity.cookie.name != params.affinity.cookie.name ||
g.affinity.cookie.path != params.affinity.cookie.path) {
LOG(ERROR) << "backend: affinity: multiple different affinity "
"configurations found in a single group";
return -1;
}
} }
// If at least one backend requires frontend TLS connection, // If at least one backend requires frontend TLS connection,
// enable it for all backends sharing the same pattern. // enable it for all backends sharing the same pattern.
@ -983,7 +1020,15 @@ int parse_mapping(Config *config, DownstreamAddrConfig &addr,
addr_groups.emplace_back(pattern); addr_groups.emplace_back(pattern);
auto &g = addr_groups.back(); auto &g = addr_groups.back();
g.addrs.push_back(addr); g.addrs.push_back(addr);
g.affinity = params.affinity; g.affinity.type = params.affinity.type;
if (params.affinity.type == AFFINITY_COOKIE) {
g.affinity.cookie.name =
make_string_ref(downstreamconf.balloc, params.affinity.cookie.name);
if (!params.affinity.cookie.path.empty()) {
g.affinity.cookie.path =
make_string_ref(downstreamconf.balloc, params.affinity.cookie.path);
}
}
g.redirect_if_not_tls = params.redirect_if_not_tls; g.redirect_if_not_tls = params.redirect_if_not_tls;
if (pattern[0] == '*') { if (pattern[0] == '*') {
@ -3823,7 +3868,7 @@ int configure_downstream_group(Config *config, bool http2_proxy,
} }
} }
if (g.affinity == AFFINITY_IP) { if (g.affinity.type != AFFINITY_NONE) {
size_t idx = 0; size_t idx = 0;
for (auto &addr : g.addrs) { for (auto &addr : g.addrs) {
StringRef key; StringRef key;

View File

@ -356,6 +356,19 @@ enum shrpx_session_affinity {
AFFINITY_NONE, AFFINITY_NONE,
// Client IP affinity // Client IP affinity
AFFINITY_IP, AFFINITY_IP,
// Cookie based affinity
AFFINITY_COOKIE,
};
struct AffinityConfig {
// Type of session affinity.
shrpx_session_affinity type;
struct {
// Name of a cookie to use.
StringRef name;
// Path which a cookie is applied to.
StringRef path;
} cookie;
}; };
enum shrpx_forwarded_param { enum shrpx_forwarded_param {
@ -449,15 +462,15 @@ struct AffinityHash {
struct DownstreamAddrGroupConfig { struct DownstreamAddrGroupConfig {
DownstreamAddrGroupConfig(const StringRef &pattern) DownstreamAddrGroupConfig(const StringRef &pattern)
: pattern(pattern), affinity(AFFINITY_NONE), redirect_if_not_tls(false) {} : pattern(pattern), affinity{AFFINITY_NONE}, redirect_if_not_tls(false) {}
StringRef pattern; StringRef pattern;
std::vector<DownstreamAddrConfig> addrs; std::vector<DownstreamAddrConfig> addrs;
// Bunch of session affinity hash. Only used if affinity == // Bunch of session affinity hash. Only used if affinity ==
// AFFINITY_IP. // AFFINITY_IP.
std::vector<AffinityHash> affinity_hash; std::vector<AffinityHash> affinity_hash;
// Session affinity // Cookie based session affinity configuration.
shrpx_session_affinity affinity; AffinityConfig affinity;
// true if this group requires that client connection must be TLS, // true if this group requires that client connection must be TLS,
// and the request must be redirected to https URI. // and the request must be redirected to https URI.
bool redirect_if_not_tls; bool redirect_if_not_tls;

View File

@ -131,6 +131,7 @@ Downstream::Downstream(Upstream *upstream, MemchunkPool *mcpool,
assoc_stream_id_(-1), assoc_stream_id_(-1),
downstream_stream_id_(-1), downstream_stream_id_(-1),
response_rst_stream_error_code_(NGHTTP2_NO_ERROR), response_rst_stream_error_code_(NGHTTP2_NO_ERROR),
affinity_cookie_(0),
request_state_(INITIAL), request_state_(INITIAL),
response_state_(INITIAL), response_state_(INITIAL),
dispatch_state_(DISPATCH_NONE), dispatch_state_(DISPATCH_NONE),
@ -140,7 +141,8 @@ Downstream::Downstream(Upstream *upstream, MemchunkPool *mcpool,
expect_final_response_(false), expect_final_response_(false),
request_pending_(false), request_pending_(false),
request_header_sent_(false), request_header_sent_(false),
accesslog_written_(false) { accesslog_written_(false),
new_affinity_cookie_(false) {
auto &timeoutconf = get_config()->http2.timeout; auto &timeoutconf = get_config()->http2.timeout;
@ -305,6 +307,49 @@ StringRef Downstream::assemble_request_cookie() {
return StringRef{iov.base, p}; return StringRef{iov.base, p};
} }
uint32_t Downstream::find_affinity_cookie(const StringRef &name) {
for (auto &kv : req_.fs.headers()) {
if (kv.token != http2::HD_COOKIE) {
continue;
}
for (auto it = std::begin(kv.value); it != std::end(kv.value);) {
if (*it == '\t' || *it == ' ' || *it == ';') {
++it;
continue;
}
auto end = std::find(it, std::end(kv.value), '=');
if (end == std::end(kv.value)) {
return 0;
}
if (!util::streq(name, StringRef{it, end})) {
it = std::find(it, std::end(kv.value), ';');
continue;
}
it = std::find(end + 1, std::end(kv.value), ';');
auto val = StringRef{end + 1, it};
if (val.size() != 8) {
return 0;
}
uint32_t h = 0;
for (auto c : val) {
auto n = util::hex_to_uint(c);
if (n == 256) {
return 0;
}
h <<= 4;
h += n;
}
affinity_cookie_ = h;
return h;
}
}
return 0;
}
size_t Downstream::count_crumble_request_cookie() { size_t Downstream::count_crumble_request_cookie() {
size_t n = 0; size_t n = 0;
for (auto &kv : req_.fs.headers()) { for (auto &kv : req_.fs.headers()) {
@ -995,4 +1040,16 @@ const DownstreamAddr *Downstream::get_addr() const { return addr_; }
void Downstream::set_accesslog_written(bool f) { accesslog_written_ = f; } void Downstream::set_accesslog_written(bool f) { accesslog_written_ = f; }
void Downstream::renew_affinity_cookie(uint32_t h) {
affinity_cookie_ = h;
new_affinity_cookie_ = true;
}
uint32_t Downstream::get_affinity_cookie_to_send() const {
if (new_affinity_cookie_) {
return affinity_cookie_;
}
return 0;
}
} // namespace shrpx } // namespace shrpx

View File

@ -412,6 +412,18 @@ public:
void set_accesslog_written(bool f); void set_accesslog_written(bool f);
// Finds affinity cookie from request header fields. The name of
// cookie is given in |name|. If an affinity cookie is found, it is
// assigned to a member function, and is returned. If it is not
// found, or is malformed, returns 0.
uint32_t find_affinity_cookie(const StringRef &name);
// Set |h| as affinity cookie.
void renew_affinity_cookie(uint32_t h);
// Returns affinity cookie to send. If it does not need to be sent,
// for example, because the value is retrieved from a request header
// field, returns 0.
uint32_t get_affinity_cookie_to_send() const;
enum { enum {
EVENT_ERROR = 0x1, EVENT_ERROR = 0x1,
EVENT_TIMEOUT = 0x2, EVENT_TIMEOUT = 0x2,
@ -474,6 +486,8 @@ private:
int32_t downstream_stream_id_; int32_t downstream_stream_id_;
// RST_STREAM error_code from downstream HTTP2 connection // RST_STREAM error_code from downstream HTTP2 connection
uint32_t response_rst_stream_error_code_; uint32_t response_rst_stream_error_code_;
// An affinity cookie value.
uint32_t affinity_cookie_;
// request state // request state
int request_state_; int request_state_;
// response state // response state
@ -497,6 +511,8 @@ private:
bool request_header_sent_; bool request_header_sent_;
// true if access.log has been written. // true if access.log has been written.
bool accesslog_written_; bool accesslog_written_;
// true if affinity cookie is generated for this request.
bool new_affinity_cookie_;
}; };
} // namespace shrpx } // namespace shrpx

View File

@ -189,4 +189,38 @@ void test_downstream_supports_non_final_response(void) {
CU_ASSERT(!d.supports_non_final_response()); CU_ASSERT(!d.supports_non_final_response());
} }
void test_downstream_find_affinity_cookie(void) {
Downstream d(nullptr, nullptr, 0);
auto &req = d.request();
req.fs.add_header_token(StringRef::from_lit("cookie"), StringRef{}, false,
http2::HD_COOKIE);
req.fs.add_header_token(StringRef::from_lit("cookie"),
StringRef::from_lit("a=b;;c=d"), false,
http2::HD_COOKIE);
req.fs.add_header_token(StringRef::from_lit("content-length"),
StringRef::from_lit("599"), false,
http2::HD_CONTENT_LENGTH);
req.fs.add_header_token(StringRef::from_lit("cookie"),
StringRef::from_lit("lb=deadbeef;LB=f1f2f3f4"), false,
http2::HD_COOKIE);
req.fs.add_header_token(StringRef::from_lit("cookie"),
StringRef::from_lit("short=e1e2e3e"), false,
http2::HD_COOKIE);
uint32_t aff;
aff = d.find_affinity_cookie(StringRef::from_lit("lb"));
CU_ASSERT(0xdeadbeef == aff);
aff = d.find_affinity_cookie(StringRef::from_lit("LB"));
CU_ASSERT(0xf1f2f3f4 == aff);
aff = d.find_affinity_cookie(StringRef::from_lit("short"));
CU_ASSERT(0 == aff);
}
} // namespace shrpx } // namespace shrpx

View File

@ -37,6 +37,7 @@ void test_downstream_crumble_request_cookie(void);
void test_downstream_assemble_request_cookie(void); void test_downstream_assemble_request_cookie(void);
void test_downstream_rewrite_location_response_header(void); void test_downstream_rewrite_location_response_header(void);
void test_downstream_supports_non_final_response(void); void test_downstream_supports_non_final_response(void);
void test_downstream_find_affinity_cookie(void);
} // namespace shrpx } // namespace shrpx

View File

@ -164,6 +164,41 @@ ssize_t select_padding_callback(nghttp2_session *session,
return std::min(max_payload, frame->hd.length + get_config()->padding); return std::min(max_payload, frame->hd.length + get_config()->padding);
} }
StringRef create_affinity_cookie(BlockAllocator &balloc, const StringRef &name,
uint32_t affinity_cookie,
const StringRef &path, bool secure) {
static constexpr auto PATH_PREFIX = StringRef::from_lit("; Path=");
static constexpr auto SECURE = StringRef::from_lit("; Secure");
// <name>=<value>[; Path=<path>][; Secure]
size_t len = name.size() + 1 + 8;
if (!path.empty()) {
len += PATH_PREFIX.size() + path.size();
}
if (secure) {
len += SECURE.size();
}
auto iov = make_byte_ref(balloc, len + 1);
auto p = iov.base;
p = std::copy(std::begin(name), std::end(name), p);
*p++ = '=';
affinity_cookie = htonl(affinity_cookie);
p = util::format_hex(p,
StringRef{reinterpret_cast<uint8_t *>(&affinity_cookie),
reinterpret_cast<uint8_t *>(&affinity_cookie) +
sizeof(affinity_cookie)});
if (!path.empty()) {
p = std::copy(std::begin(PATH_PREFIX), std::end(PATH_PREFIX), p);
p = std::copy(std::begin(path), std::end(path), p);
}
if (secure) {
p = std::copy(std::begin(SECURE), std::end(SECURE), p);
}
*p = '\0';
return StringRef{iov.base, p};
}
} // namespace http } // namespace http
} // namespace shrpx } // namespace shrpx

View File

@ -66,6 +66,13 @@ ssize_t select_padding_callback(nghttp2_session *session,
const nghttp2_frame *frame, size_t max_payload, const nghttp2_frame *frame, size_t max_payload,
void *user_data); void *user_data);
// Creates set-cookie-string for cookie based affinity. If |path| is
// not empty, "; <path>" is added. If |secure| is true, "; Secure" is
// added.
StringRef create_affinity_cookie(BlockAllocator &balloc, const StringRef &name,
uint32_t affinity_cookie,
const StringRef &path, bool secure);
} // namespace http } // namespace http
} // namespace shrpx } // namespace shrpx

View File

@ -1657,9 +1657,9 @@ int Http2Upstream::on_downstream_header_complete(Downstream *downstream) {
} }
auto nva = std::vector<nghttp2_nv>(); auto nva = std::vector<nghttp2_nv>();
// 4 means :status and possible server, via and x-http2-push header // 5 means :status and possible server, via, x-http2-push, and
// field. // set-cookie (for affinity cookie) header field.
nva.reserve(resp.fs.headers().size() + 4 + nva.reserve(resp.fs.headers().size() + 5 +
httpconf.add_response_headers.size()); httpconf.add_response_headers.size());
auto response_status = http2::stringify_status(balloc, resp.http_status); auto response_status = http2::stringify_status(balloc, resp.http_status);
@ -1700,6 +1700,21 @@ int Http2Upstream::on_downstream_header_complete(Downstream *downstream) {
} }
} }
if (req.method != HTTP_CONNECT || !downstream->get_upgraded()) {
auto affinity_cookie = downstream->get_affinity_cookie_to_send();
if (affinity_cookie) {
auto dconn = downstream->get_downstream_connection();
assert(dconn);
auto &group = dconn->get_downstream_addr_group();
auto &shared_addr = group->shared_addr;
auto &cookieconf = shared_addr->affinity.cookie;
auto cookie_str =
http::create_affinity_cookie(balloc, cookieconf.name, affinity_cookie,
cookieconf.path, req.scheme == "https");
nva.push_back(http2::make_nv_ls_nocopy("set-cookie", cookie_str));
}
}
auto via = resp.fs.header(http2::HD_VIA); auto via = resp.fs.header(http2::HD_VIA);
if (httpconf.no_via) { if (httpconf.no_via) {
if (via) { if (via) {

View File

@ -269,8 +269,9 @@ int HttpDownstreamConnection::initiate_connection() {
// initial_addr_idx_. // initial_addr_idx_.
size_t temp_idx = initial_addr_idx_; size_t temp_idx = initial_addr_idx_;
auto &next_downstream = auto &next_downstream = shared_addr->affinity.type == AFFINITY_NONE
shared_addr->affinity == AFFINITY_NONE ? shared_addr->next : temp_idx; ? shared_addr->next
: temp_idx;
auto end = next_downstream; auto end = next_downstream;
for (;;) { for (;;) {
auto check_dns_result = dns_query_.get() != nullptr; auto check_dns_result = dns_query_.get() != nullptr;
@ -757,7 +758,7 @@ void remove_from_pool(HttpDownstreamConnection *dconn) {
auto &group = dconn->get_downstream_addr_group(); auto &group = dconn->get_downstream_addr_group();
auto &shared_addr = group->shared_addr; auto &shared_addr = group->shared_addr;
if (shared_addr->affinity == AFFINITY_NONE) { if (shared_addr->affinity.type == AFFINITY_NONE) {
auto &dconn_pool = auto &dconn_pool =
dconn->get_downstream_addr_group()->shared_addr->dconn_pool; dconn->get_downstream_addr_group()->shared_addr->dconn_pool;
dconn_pool.remove_downstream_connection(dconn); dconn_pool.remove_downstream_connection(dconn);

View File

@ -91,4 +91,31 @@ void test_shrpx_http_create_via_header_value(void) {
CU_ASSERT(("2 nghttpx" == StringRef{std::begin(buf), end})); CU_ASSERT(("2 nghttpx" == StringRef{std::begin(buf), end}));
} }
void test_shrpx_http_create_affinity_cookie(void) {
BlockAllocator balloc(1024, 1024);
StringRef c;
c = http::create_affinity_cookie(balloc, StringRef::from_lit("cookie-val"),
0xf1e2d3c4u, StringRef{}, false);
CU_ASSERT("cookie-val=f1e2d3c4" == c);
c = http::create_affinity_cookie(balloc, StringRef::from_lit("alpha"),
0x00000000u, StringRef{}, true);
CU_ASSERT("alpha=00000000; Secure" == c);
c = http::create_affinity_cookie(balloc, StringRef::from_lit("bravo"),
0x01111111u, StringRef::from_lit("bar"),
false);
CU_ASSERT("bravo=01111111; Path=bar" == c);
c = http::create_affinity_cookie(balloc, StringRef::from_lit("charlie"),
0x01111111u, StringRef::from_lit("bar"),
true);
CU_ASSERT("charlie=01111111; Path=bar; Secure" == c);
}
} // namespace shrpx } // namespace shrpx

View File

@ -33,6 +33,7 @@ namespace shrpx {
void test_shrpx_http_create_forwarded(void); void test_shrpx_http_create_forwarded(void);
void test_shrpx_http_create_via_header_value(void); void test_shrpx_http_create_via_header_value(void);
void test_shrpx_http_create_affinity_cookie(void);
} // namespace shrpx } // namespace shrpx

View File

@ -1147,6 +1147,23 @@ int HttpsUpstream::on_downstream_header_complete(Downstream *downstream) {
} }
} }
if (req.method != HTTP_CONNECT || !downstream->get_upgraded()) {
auto affinity_cookie = downstream->get_affinity_cookie_to_send();
if (affinity_cookie) {
auto dconn = downstream->get_downstream_connection();
assert(dconn);
auto &group = dconn->get_downstream_addr_group();
auto &shared_addr = group->shared_addr;
auto &cookieconf = shared_addr->affinity.cookie;
auto cookie_str =
http::create_affinity_cookie(balloc, cookieconf.name, affinity_cookie,
cookieconf.path, req.scheme == "https");
buf->append("Set-Cookie: ");
buf->append(cookie_str);
buf->append("\r\n");
}
}
auto via = resp.fs.header(http2::HD_VIA); auto via = resp.fs.header(http2::HD_VIA);
if (httpconf.no_via) { if (httpconf.no_via) {
if (via) { if (via) {

View File

@ -76,11 +76,17 @@ bool match_shared_downstream_addr(
return false; return false;
} }
if (lhs->affinity != rhs->affinity || if (lhs->affinity.type != rhs->affinity.type ||
lhs->redirect_if_not_tls != rhs->redirect_if_not_tls) { lhs->redirect_if_not_tls != rhs->redirect_if_not_tls) {
return false; return false;
} }
if (lhs->affinity.type == AFFINITY_COOKIE &&
(lhs->affinity.cookie.name != rhs->affinity.cookie.name ||
lhs->affinity.cookie.path != rhs->affinity.cookie.path)) {
return false;
}
auto used = std::vector<bool>(lhs->addrs.size()); auto used = std::vector<bool>(lhs->addrs.size());
for (auto &a : lhs->addrs) { for (auto &a : lhs->addrs) {
@ -156,7 +162,7 @@ void Worker::replace_downstream_config(
auto &shared_addr = g->shared_addr; auto &shared_addr = g->shared_addr;
if (shared_addr->affinity == AFFINITY_NONE) { if (shared_addr->affinity.type == AFFINITY_NONE) {
shared_addr->dconn_pool.remove_all(); shared_addr->dconn_pool.remove_all();
continue; continue;
} }
@ -186,7 +192,15 @@ void Worker::replace_downstream_config(
auto shared_addr = std::make_shared<SharedDownstreamAddr>(); auto shared_addr = std::make_shared<SharedDownstreamAddr>();
shared_addr->addrs.resize(src.addrs.size()); shared_addr->addrs.resize(src.addrs.size());
shared_addr->affinity = src.affinity; shared_addr->affinity.type = src.affinity.type;
if (src.affinity.type == AFFINITY_COOKIE) {
shared_addr->affinity.cookie.name =
make_string_ref(shared_addr->balloc, src.affinity.cookie.name);
if (!src.affinity.cookie.path.empty()) {
shared_addr->affinity.cookie.path =
make_string_ref(shared_addr->balloc, src.affinity.cookie.path);
}
}
shared_addr->affinity_hash = src.affinity_hash; shared_addr->affinity_hash = src.affinity_hash;
shared_addr->redirect_if_not_tls = src.redirect_if_not_tls; shared_addr->redirect_if_not_tls = src.redirect_if_not_tls;
@ -268,7 +282,7 @@ void Worker::replace_downstream_config(
shared_addr->http1_pri.weight = num_http1; shared_addr->http1_pri.weight = num_http1;
shared_addr->http2_pri.weight = num_http2; shared_addr->http2_pri.weight = num_http2;
if (shared_addr->affinity != AFFINITY_NONE) { if (shared_addr->affinity.type != AFFINITY_NONE) {
for (auto &addr : shared_addr->addrs) { for (auto &addr : shared_addr->addrs) {
addr.dconn_pool = make_unique<DownstreamConnectionPool>(); addr.dconn_pool = make_unique<DownstreamConnectionPool>();
} }

View File

@ -133,10 +133,10 @@ struct WeightedPri {
struct SharedDownstreamAddr { struct SharedDownstreamAddr {
SharedDownstreamAddr() SharedDownstreamAddr()
: balloc(1024, 1024), : balloc(1024, 1024),
affinity{AFFINITY_NONE},
next{0}, next{0},
http1_pri{}, http1_pri{},
http2_pri{}, http2_pri{},
affinity{AFFINITY_NONE},
redirect_if_not_tls{false} {} redirect_if_not_tls{false} {}
SharedDownstreamAddr(const SharedDownstreamAddr &) = delete; SharedDownstreamAddr(const SharedDownstreamAddr &) = delete;
@ -161,6 +161,8 @@ struct SharedDownstreamAddr {
// wise. // wise.
DList<Http2Session> http2_avail_freelist; DList<Http2Session> http2_avail_freelist;
DownstreamConnectionPool dconn_pool; DownstreamConnectionPool dconn_pool;
// Configuration for session affinity
AffinityConfig affinity;
// Next http/1.1 downstream address index in addrs. // Next http/1.1 downstream address index in addrs.
size_t next; size_t next;
// http1_pri and http2_pri are used to which protocols are used // http1_pri and http2_pri are used to which protocols are used
@ -171,7 +173,6 @@ struct SharedDownstreamAddr {
WeightedPri http1_pri; WeightedPri http1_pri;
WeightedPri http2_pri; WeightedPri http2_pri;
// Session affinity // Session affinity
shrpx_session_affinity affinity;
// true if this group requires that client connection must be TLS, // true if this group requires that client connection must be TLS,
// and the request must be redirected to https URI. // and the request must be redirected to https URI.
bool redirect_if_not_tls; bool redirect_if_not_tls;

View File

@ -189,7 +189,7 @@ uint32_t hex_to_uint(char c) {
if (c <= 'z') { if (c <= 'z') {
return c - 'a' + 10; return c - 'a' + 10;
} }
return c; return 256;
} }
StringRef quote_string(BlockAllocator &balloc, const StringRef &target) { StringRef quote_string(BlockAllocator &balloc, const StringRef &target) {
@ -415,10 +415,6 @@ char upcase(char c) {
} }
} }
namespace {
constexpr char LOWER_XDIGITS[] = "0123456789abcdef";
} // namespace
std::string format_hex(const unsigned char *s, size_t len) { std::string format_hex(const unsigned char *s, size_t len) {
std::string res; std::string res;
res.resize(len * 2); res.resize(len * 2);

View File

@ -96,8 +96,8 @@ bool in_token(char c);
bool in_attr_char(char c); bool in_attr_char(char c);
// Returns integer corresponding to hex notation |c|. It is undefined // Returns integer corresponding to hex notation |c|. If
// if is_hex_digit(c) is false. // is_hex_digit(c) is false, it returns 256.
uint32_t hex_to_uint(char c); uint32_t hex_to_uint(char c);
std::string percent_encode(const unsigned char *target, size_t len); std::string percent_encode(const unsigned char *target, size_t len);
@ -152,6 +152,19 @@ template <size_t N> std::string format_hex(const std::array<uint8_t, N> &s) {
StringRef format_hex(BlockAllocator &balloc, const StringRef &s); StringRef format_hex(BlockAllocator &balloc, const StringRef &s);
static constexpr char LOWER_XDIGITS[] = "0123456789abcdef";
template <typename OutputIt>
OutputIt format_hex(OutputIt it, const StringRef &s) {
for (auto cc : s) {
uint8_t c = cc;
*it++ = LOWER_XDIGITS[c >> 4];
*it++ = LOWER_XDIGITS[c & 0xf];
}
return it;
}
// decode_hex decodes hex string |s|, returns the decoded byte string. // decode_hex decodes hex string |s|, returns the decoded byte string.
// This function assumes |s| is hex string, that is is_hex_string(s) // This function assumes |s| is hex string, that is is_hex_string(s)
// == true. // == true.