Coverage Report

Created: 2024-09-19 09:45

/proc/self/cwd/source/common/http/hash_policy.cc
Line
Count
Source (jump to first uncovered line)
1
#include "source/common/http/hash_policy.h"
2
3
#include <string>
4
5
#include "envoy/common/hashable.h"
6
#include "envoy/config/route/v3/route_components.pb.h"
7
8
#include "source/common/common/matchers.h"
9
#include "source/common/common/regex.h"
10
#include "source/common/http/utility.h"
11
#include "source/common/runtime/runtime_features.h"
12
13
#include "absl/strings/str_cat.h"
14
15
namespace Envoy {
16
namespace Http {
17
18
class HashMethodImplBase : public HashPolicyImpl::HashMethod {
19
public:
20
11.9k
  explicit HashMethodImplBase(bool terminal) : terminal_(terminal) {}
21
22
0
  bool terminal() const override { return terminal_; }
23
24
private:
25
  const bool terminal_;
26
};
27
28
class HeaderHashMethod : public HashMethodImplBase {
29
public:
30
  HeaderHashMethod(const envoy::config::route::v3::RouteAction::HashPolicy::Header& header,
31
                   bool terminal, Regex::Engine& regex_engine)
32
3.61k
      : HashMethodImplBase(terminal), header_name_(header.header_name()) {
33
3.61k
    if (header.has_regex_rewrite()) {
34
2.32k
      const auto& rewrite_spec = header.regex_rewrite();
35
2.32k
      regex_rewrite_ = Regex::Utility::parseRegex(rewrite_spec.pattern(), regex_engine);
36
2.32k
      regex_rewrite_substitution_ = rewrite_spec.substitution();
37
2.32k
    }
38
3.61k
  }
39
40
  absl::optional<uint64_t> evaluate(const Network::Address::Instance*,
41
                                    const RequestHeaderMap& headers,
42
                                    const HashPolicy::AddCookieCallback,
43
0
                                    const StreamInfo::FilterStateSharedPtr) const override {
44
0
    absl::optional<uint64_t> hash;
45
46
0
    const auto header = headers.get(header_name_);
47
0
    if (!header.empty()) {
48
0
      absl::InlinedVector<absl::string_view, 1> header_values;
49
0
      size_t num_headers_to_hash = header.size();
50
0
      header_values.reserve(num_headers_to_hash);
51
52
0
      for (size_t i = 0; i < num_headers_to_hash; i++) {
53
0
        header_values.push_back(header[i]->value().getStringView());
54
0
      }
55
56
0
      absl::InlinedVector<std::string, 1> rewritten_header_values;
57
0
      if (regex_rewrite_ != nullptr) {
58
0
        rewritten_header_values.reserve(num_headers_to_hash);
59
0
        for (auto& value : header_values) {
60
0
          rewritten_header_values.push_back(
61
0
              regex_rewrite_->replaceAll(value, regex_rewrite_substitution_));
62
0
          value = rewritten_header_values.back();
63
0
        }
64
0
      }
65
66
      // Ensure generating same hash value for different order header values.
67
      // For example, generates the same hash value for {"foo","bar"} and {"bar","foo"}
68
0
      std::sort(header_values.begin(), header_values.end());
69
0
      hash = HashUtil::xxHash64(absl::MakeSpan(header_values));
70
0
    }
71
0
    return hash;
72
0
  }
73
74
private:
75
  const LowerCaseString header_name_;
76
  Regex::CompiledMatcherPtr regex_rewrite_{};
77
  std::string regex_rewrite_substitution_{};
78
};
79
80
class CookieHashMethod : public HashMethodImplBase {
81
public:
82
  CookieHashMethod(const std::string& key, const std::string& path,
83
                   const absl::optional<std::chrono::seconds>& ttl, bool terminal,
84
                   const CookieAttributeRefVector attributes)
85
2.04k
      : HashMethodImplBase(terminal), key_(key), path_(path), ttl_(ttl) {
86
3.51k
    for (const auto& attribute : attributes) {
87
3.51k
      attributes_.push_back(attribute);
88
3.51k
    }
89
2.04k
  }
90
91
  absl::optional<uint64_t> evaluate(const Network::Address::Instance*,
92
                                    const RequestHeaderMap& headers,
93
                                    const HashPolicy::AddCookieCallback add_cookie,
94
0
                                    const StreamInfo::FilterStateSharedPtr) const override {
95
0
    absl::optional<uint64_t> hash;
96
0
    std::string value = Utility::parseCookieValue(headers, key_);
97
0
    if (value.empty() && ttl_.has_value()) {
98
0
      CookieAttributeRefVector attributes;
99
0
      for (const auto& attribute : attributes_) {
100
0
        attributes.push_back(attribute);
101
0
      }
102
0
      value = add_cookie(key_, path_, ttl_.value(), attributes);
103
0
      hash = HashUtil::xxHash64(value);
104
105
0
    } else if (!value.empty()) {
106
0
      hash = HashUtil::xxHash64(value);
107
0
    }
108
0
    return hash;
109
0
  }
110
111
private:
112
  const std::string key_;
113
  const std::string path_;
114
  const absl::optional<std::chrono::seconds> ttl_;
115
  std::vector<CookieAttribute> attributes_;
116
};
117
118
class IpHashMethod : public HashMethodImplBase {
119
public:
120
1.44k
  IpHashMethod(bool terminal) : HashMethodImplBase(terminal) {}
121
122
  absl::optional<uint64_t> evaluate(const Network::Address::Instance* downstream_addr,
123
                                    const RequestHeaderMap&, const HashPolicy::AddCookieCallback,
124
0
                                    const StreamInfo::FilterStateSharedPtr) const override {
125
0
    if (downstream_addr == nullptr) {
126
0
      return absl::nullopt;
127
0
    }
128
0
    auto* downstream_ip = downstream_addr->ip();
129
0
    if (downstream_ip == nullptr) {
130
0
      return absl::nullopt;
131
0
    }
132
0
    const auto& downstream_addr_str = downstream_ip->addressAsString();
133
0
    if (downstream_addr_str.empty()) {
134
0
      return absl::nullopt;
135
0
    }
136
0
    return HashUtil::xxHash64(downstream_addr_str);
137
0
  }
138
};
139
140
class QueryParameterHashMethod : public HashMethodImplBase {
141
public:
142
  QueryParameterHashMethod(const std::string& parameter_name, bool terminal)
143
2.34k
      : HashMethodImplBase(terminal), parameter_name_(parameter_name) {}
144
145
  absl::optional<uint64_t> evaluate(const Network::Address::Instance*,
146
                                    const RequestHeaderMap& headers,
147
                                    const HashPolicy::AddCookieCallback,
148
0
                                    const StreamInfo::FilterStateSharedPtr) const override {
149
0
    absl::optional<uint64_t> hash;
150
151
0
    const HeaderEntry* header = headers.Path();
152
0
    if (header) {
153
0
      Http::Utility::QueryParamsMulti query_parameters =
154
0
          Http::Utility::QueryParamsMulti::parseQueryString(header->value().getStringView());
155
0
      const auto val = query_parameters.getFirstValue(parameter_name_);
156
0
      if (val.has_value()) {
157
0
        hash = HashUtil::xxHash64(val.value());
158
0
      }
159
0
    }
160
0
    return hash;
161
0
  }
162
163
private:
164
  const std::string parameter_name_;
165
};
166
167
class FilterStateHashMethod : public HashMethodImplBase {
168
public:
169
  FilterStateHashMethod(const std::string& key, bool terminal)
170
2.53k
      : HashMethodImplBase(terminal), key_(key) {}
171
172
  absl::optional<uint64_t>
173
  evaluate(const Network::Address::Instance*, const RequestHeaderMap&,
174
           const HashPolicy::AddCookieCallback,
175
0
           const StreamInfo::FilterStateSharedPtr filter_state) const override {
176
0
    if (auto typed_state = filter_state->getDataReadOnly<Hashable>(key_); typed_state != nullptr) {
177
0
      return typed_state->hash();
178
0
    }
179
0
    return absl::nullopt;
180
0
  }
181
182
private:
183
  const std::string key_;
184
};
185
186
HashPolicyImpl::HashPolicyImpl(
187
    absl::Span<const envoy::config::route::v3::RouteAction::HashPolicy* const> hash_policies,
188
7.36k
    Regex::Engine& regex_engine) {
189
190
7.36k
  hash_impls_.reserve(hash_policies.size());
191
14.0k
  for (auto* hash_policy : hash_policies) {
192
14.0k
    switch (hash_policy->policy_specifier_case()) {
193
3.61k
    case envoy::config::route::v3::RouteAction::HashPolicy::PolicySpecifierCase::kHeader:
194
3.61k
      hash_impls_.emplace_back(
195
3.61k
          new HeaderHashMethod(hash_policy->header(), hash_policy->terminal(), regex_engine));
196
3.61k
      break;
197
2.04k
    case envoy::config::route::v3::RouteAction::HashPolicy::PolicySpecifierCase::kCookie: {
198
2.04k
      absl::optional<std::chrono::seconds> ttl;
199
2.04k
      if (hash_policy->cookie().has_ttl()) {
200
215
        ttl = std::chrono::seconds(hash_policy->cookie().ttl().seconds());
201
215
      }
202
2.04k
      std::vector<CookieAttribute> attributes;
203
3.51k
      for (const auto& attribute : hash_policy->cookie().attributes()) {
204
3.51k
        attributes.push_back({attribute.name(), attribute.value()});
205
3.51k
      }
206
2.04k
      CookieAttributeRefVector ref_attributes;
207
3.51k
      for (const auto& attribute : attributes) {
208
3.51k
        ref_attributes.push_back(attribute);
209
3.51k
      }
210
2.04k
      hash_impls_.emplace_back(new CookieHashMethod(hash_policy->cookie().name(),
211
2.04k
                                                    hash_policy->cookie().path(), ttl,
212
2.04k
                                                    hash_policy->terminal(), ref_attributes));
213
2.04k
      break;
214
0
    }
215
3.47k
    case envoy::config::route::v3::RouteAction::HashPolicy::PolicySpecifierCase::
216
3.47k
        kConnectionProperties:
217
3.47k
      if (hash_policy->connection_properties().source_ip()) {
218
1.44k
        hash_impls_.emplace_back(new IpHashMethod(hash_policy->terminal()));
219
1.44k
      }
220
3.47k
      break;
221
2.34k
    case envoy::config::route::v3::RouteAction::HashPolicy::PolicySpecifierCase::kQueryParameter:
222
2.34k
      hash_impls_.emplace_back(new QueryParameterHashMethod(hash_policy->query_parameter().name(),
223
2.34k
                                                            hash_policy->terminal()));
224
2.34k
      break;
225
2.53k
    case envoy::config::route::v3::RouteAction::HashPolicy::PolicySpecifierCase::kFilterState:
226
2.53k
      hash_impls_.emplace_back(
227
2.53k
          new FilterStateHashMethod(hash_policy->filter_state().key(), hash_policy->terminal()));
228
2.53k
      break;
229
0
    case envoy::config::route::v3::RouteAction::HashPolicy::PolicySpecifierCase::
230
0
        POLICY_SPECIFIER_NOT_SET:
231
0
      PANIC("hash policy not set");
232
14.0k
    }
233
14.0k
  }
234
7.36k
}
235
236
absl::optional<uint64_t>
237
HashPolicyImpl::generateHash(const Network::Address::Instance* downstream_addr,
238
                             const RequestHeaderMap& headers, const AddCookieCallback add_cookie,
239
0
                             const StreamInfo::FilterStateSharedPtr filter_state) const {
240
0
  absl::optional<uint64_t> hash;
241
0
  for (const HashMethodPtr& hash_impl : hash_impls_) {
242
0
    const absl::optional<uint64_t> new_hash =
243
0
        hash_impl->evaluate(downstream_addr, headers, add_cookie, filter_state);
244
0
    if (new_hash) {
245
      // Rotating the old value prevents duplicate hash rules from cancelling each other out
246
      // and preserves all of the entropy
247
0
      const uint64_t old_value = hash ? ((hash.value() << 1) | (hash.value() >> 63)) : 0;
248
0
      hash = old_value ^ new_hash.value();
249
0
    }
250
    // If the policy is a terminal policy and a hash has been generated, ignore
251
    // the rest of the hash policies.
252
0
    if (hash_impl->terminal() && hash) {
253
0
      break;
254
0
    }
255
0
  }
256
0
  return hash;
257
0
}
258
259
} // namespace Http
260
} // namespace Envoy