/proc/self/cwd/source/common/http/header_utility.cc
Line | Count | Source (jump to first uncovered line) |
1 | | #include "source/common/http/header_utility.h" |
2 | | |
3 | | #include "envoy/config/route/v3/route_components.pb.h" |
4 | | |
5 | | #include "source/common/common/json_escape_string.h" |
6 | | #include "source/common/common/matchers.h" |
7 | | #include "source/common/common/regex.h" |
8 | | #include "source/common/common/utility.h" |
9 | | #include "source/common/http/character_set_validation.h" |
10 | | #include "source/common/http/header_map_impl.h" |
11 | | #include "source/common/http/utility.h" |
12 | | #include "source/common/protobuf/utility.h" |
13 | | #include "source/common/runtime/runtime_features.h" |
14 | | |
15 | | #include "absl/strings/match.h" |
16 | | #include "nghttp2/nghttp2.h" |
17 | | #include "quiche/common/structured_headers.h" |
18 | | #include "quiche/http2/adapter/header_validator.h" |
19 | | |
20 | | namespace Envoy { |
21 | | namespace Http { |
22 | | |
23 | | struct SharedResponseCodeDetailsValues { |
24 | | const absl::string_view InvalidAuthority = "http.invalid_authority"; |
25 | | const absl::string_view ConnectUnsupported = "http.connect_not_supported"; |
26 | | const absl::string_view InvalidMethod = "http.invalid_method"; |
27 | | const absl::string_view InvalidPath = "http.invalid_path"; |
28 | | const absl::string_view InvalidScheme = "http.invalid_scheme"; |
29 | | }; |
30 | | |
31 | | using SharedResponseCodeDetails = ConstSingleton<SharedResponseCodeDetailsValues>; |
32 | | |
33 | | // HeaderMatcher will consist of: |
34 | | // header_match_specifier which can be any one of exact_match, regex_match, range_match, |
35 | | // present_match, prefix_match or suffix_match. |
36 | | // Each of these also can be inverted with the invert_match option. |
37 | | // Absence of these options implies empty header value match based on header presence. |
38 | | // a.exact_match: value will be used for exact string matching. |
39 | | // b.regex_match: Match will succeed if header value matches the value specified here. |
40 | | // c.range_match: Match will succeed if header value lies within the range specified |
41 | | // here, using half open interval semantics [start,end). |
42 | | // d.present_match: Match will succeed if the header is present. |
43 | | // f.prefix_match: Match will succeed if header value matches the prefix value specified here. |
44 | | // g.suffix_match: Match will succeed if header value matches the suffix value specified here. |
45 | | HeaderUtility::HeaderData::HeaderData(const envoy::config::route::v3::HeaderMatcher& config) |
46 | | : name_(config.name()), invert_match_(config.invert_match()), |
47 | 48.0k | treat_missing_as_empty_(config.treat_missing_header_as_empty()) { |
48 | 48.0k | switch (config.header_match_specifier_case()) { |
49 | 2.34k | case envoy::config::route::v3::HeaderMatcher::HeaderMatchSpecifierCase::kExactMatch: |
50 | 2.34k | header_match_type_ = HeaderMatchType::Value; |
51 | 2.34k | value_ = config.exact_match(); |
52 | 2.34k | break; |
53 | 5.03k | case envoy::config::route::v3::HeaderMatcher::HeaderMatchSpecifierCase::kSafeRegexMatch: |
54 | 5.03k | header_match_type_ = HeaderMatchType::Regex; |
55 | 5.03k | regex_ = Regex::Utility::parseRegex(config.safe_regex_match()); |
56 | 5.03k | break; |
57 | 2.02k | case envoy::config::route::v3::HeaderMatcher::HeaderMatchSpecifierCase::kRangeMatch: |
58 | 2.02k | header_match_type_ = HeaderMatchType::Range; |
59 | 2.02k | range_.set_start(config.range_match().start()); |
60 | 2.02k | range_.set_end(config.range_match().end()); |
61 | 2.02k | break; |
62 | 1.49k | case envoy::config::route::v3::HeaderMatcher::HeaderMatchSpecifierCase::kPresentMatch: |
63 | 1.49k | header_match_type_ = HeaderMatchType::Present; |
64 | 1.49k | present_ = config.present_match(); |
65 | 1.49k | break; |
66 | 3.11k | case envoy::config::route::v3::HeaderMatcher::HeaderMatchSpecifierCase::kPrefixMatch: |
67 | 3.11k | header_match_type_ = HeaderMatchType::Prefix; |
68 | 3.11k | value_ = config.prefix_match(); |
69 | 3.11k | break; |
70 | 3.32k | case envoy::config::route::v3::HeaderMatcher::HeaderMatchSpecifierCase::kSuffixMatch: |
71 | 3.32k | header_match_type_ = HeaderMatchType::Suffix; |
72 | 3.32k | value_ = config.suffix_match(); |
73 | 3.32k | break; |
74 | 2.49k | case envoy::config::route::v3::HeaderMatcher::HeaderMatchSpecifierCase::kContainsMatch: |
75 | 2.49k | header_match_type_ = HeaderMatchType::Contains; |
76 | 2.49k | value_ = config.contains_match(); |
77 | 2.49k | break; |
78 | 6.61k | case envoy::config::route::v3::HeaderMatcher::HeaderMatchSpecifierCase::kStringMatch: |
79 | 6.61k | header_match_type_ = HeaderMatchType::StringMatch; |
80 | 6.61k | string_match_ = |
81 | 6.61k | std::make_unique<Matchers::StringMatcherImpl<envoy::type::matcher::v3::StringMatcher>>( |
82 | 6.61k | config.string_match()); |
83 | 6.61k | break; |
84 | 21.6k | case envoy::config::route::v3::HeaderMatcher::HeaderMatchSpecifierCase:: |
85 | 21.6k | HEADER_MATCH_SPECIFIER_NOT_SET: |
86 | 21.6k | FALLTHRU; |
87 | 21.6k | default: |
88 | 21.6k | header_match_type_ = HeaderMatchType::Present; |
89 | 21.6k | present_ = true; |
90 | 21.6k | break; |
91 | 48.0k | } |
92 | 48.0k | } |
93 | | |
94 | | bool HeaderUtility::matchHeaders(const HeaderMap& request_headers, |
95 | 43.9k | const std::vector<HeaderDataPtr>& config_headers) { |
96 | | // No headers to match is considered a match. |
97 | 43.9k | if (!config_headers.empty()) { |
98 | 12.5k | for (const HeaderDataPtr& cfg_header_data : config_headers) { |
99 | 12.5k | if (!matchHeaders(request_headers, *cfg_header_data)) { |
100 | 4.77k | return false; |
101 | 4.77k | } |
102 | 12.5k | } |
103 | 6.62k | } |
104 | | |
105 | 39.1k | return true; |
106 | 43.9k | } |
107 | | |
108 | | HeaderUtility::GetAllOfHeaderAsStringResult |
109 | | HeaderUtility::getAllOfHeaderAsString(const HeaderMap::GetResult& header_value, |
110 | 55.6k | absl::string_view separator) { |
111 | 55.6k | GetAllOfHeaderAsStringResult result; |
112 | | // In this case we concatenate all found headers using a delimiter before performing the |
113 | | // final match. We use an InlinedVector of absl::string_view to invoke the optimized join |
114 | | // algorithm. This requires a copying phase before we invoke join. The 3 used as the inline |
115 | | // size has been arbitrarily chosen. |
116 | | // TODO(mattklein123): Do we need to normalize any whitespace here? |
117 | 55.6k | absl::InlinedVector<absl::string_view, 3> string_view_vector; |
118 | 55.6k | string_view_vector.reserve(header_value.size()); |
119 | 286k | for (size_t i = 0; i < header_value.size(); i++) { |
120 | 231k | string_view_vector.push_back(header_value[i]->value().getStringView()); |
121 | 231k | } |
122 | 55.6k | result.result_backing_string_ = absl::StrJoin(string_view_vector, separator); |
123 | | |
124 | 55.6k | return result; |
125 | 55.6k | } |
126 | | |
127 | | HeaderUtility::GetAllOfHeaderAsStringResult |
128 | | HeaderUtility::getAllOfHeaderAsString(const HeaderMap& headers, const Http::LowerCaseString& key, |
129 | 230k | absl::string_view separator) { |
130 | 230k | GetAllOfHeaderAsStringResult result; |
131 | 230k | const auto header_value = headers.get(key); |
132 | | |
133 | 230k | if (header_value.empty()) { |
134 | | // Empty for clarity. Avoid handling the empty case in the block below if the runtime feature |
135 | | // is disabled. |
136 | 203k | } else if (header_value.size() == 1) { |
137 | 147k | result.result_ = header_value[0]->value().getStringView(); |
138 | 147k | } else { |
139 | 55.6k | return getAllOfHeaderAsString(header_value, separator); |
140 | 55.6k | } |
141 | | |
142 | 175k | return result; |
143 | 230k | } |
144 | | |
145 | 12.9k | bool HeaderUtility::matchHeaders(const HeaderMap& request_headers, const HeaderData& header_data) { |
146 | 12.9k | const auto header_value = getAllOfHeaderAsString(request_headers, header_data.name_); |
147 | | |
148 | 12.9k | if (!header_value.result().has_value() && !header_data.treat_missing_as_empty_) { |
149 | 2.46k | if (header_data.invert_match_) { |
150 | 392 | return header_data.header_match_type_ == HeaderMatchType::Present && header_data.present_; |
151 | 2.07k | } else { |
152 | 2.07k | return header_data.header_match_type_ == HeaderMatchType::Present && !header_data.present_; |
153 | 2.07k | } |
154 | 2.46k | } |
155 | | |
156 | | // If the header does not have value and the result is not returned in the |
157 | | // code above, it means treat_missing_as_empty_ is set to true and we should |
158 | | // treat the header value as empty. |
159 | 10.5k | const auto value = header_value.result().has_value() ? header_value.result().value() : ""; |
160 | 10.5k | bool match; |
161 | 10.5k | switch (header_data.header_match_type_) { |
162 | 905 | case HeaderMatchType::Value: |
163 | 905 | match = header_data.value_.empty() || value == header_data.value_; |
164 | 905 | break; |
165 | 158 | case HeaderMatchType::Regex: |
166 | 158 | match = header_data.regex_->match(value); |
167 | 158 | break; |
168 | 787 | case HeaderMatchType::Range: { |
169 | 787 | int64_t header_int_value = 0; |
170 | 787 | match = absl::SimpleAtoi(value, &header_int_value) && |
171 | 787 | header_int_value >= header_data.range_.start() && |
172 | 787 | header_int_value < header_data.range_.end(); |
173 | 787 | break; |
174 | 0 | } |
175 | 4.64k | case HeaderMatchType::Present: |
176 | 4.64k | match = header_data.present_; |
177 | 4.64k | break; |
178 | 886 | case HeaderMatchType::Prefix: |
179 | 886 | match = absl::StartsWith(value, header_data.value_); |
180 | 886 | break; |
181 | 519 | case HeaderMatchType::Suffix: |
182 | 519 | match = absl::EndsWith(value, header_data.value_); |
183 | 519 | break; |
184 | 772 | case HeaderMatchType::Contains: |
185 | 772 | match = absl::StrContains(value, header_data.value_); |
186 | 772 | break; |
187 | 1.83k | case HeaderMatchType::StringMatch: |
188 | 1.83k | match = header_data.string_match_->match(value); |
189 | 1.83k | break; |
190 | 10.5k | } |
191 | | |
192 | 10.5k | return match != header_data.invert_match_; |
193 | 10.5k | } |
194 | | |
195 | 889k | bool HeaderUtility::headerValueIsValid(const absl::string_view header_value) { |
196 | 889k | return http2::adapter::HeaderValidator::IsValidHeaderValue(header_value, |
197 | 889k | http2::adapter::ObsTextOption::kAllow); |
198 | 889k | } |
199 | | |
200 | 636k | bool HeaderUtility::headerNameIsValid(const absl::string_view header_key) { |
201 | 636k | if (!header_key.empty() && header_key[0] == ':') { |
202 | | // For HTTP/2 pseudo header, use the HTTP/2 semantics for checking validity |
203 | 425k | return nghttp2_check_header_name(reinterpret_cast<const uint8_t*>(header_key.data()), |
204 | 425k | header_key.size()) != 0; |
205 | 425k | } |
206 | | // For all other header use HTTP/1 semantics. The only difference from HTTP/2 is that |
207 | | // uppercase characters are allowed. This allows HTTP filters to add header with mixed |
208 | | // case names. The HTTP/1 codec will send as is, as uppercase characters are allowed. |
209 | | // However the HTTP/2 codec will NOT convert these to lowercase when serializing the |
210 | | // header map, thus producing an invalid request. |
211 | | // TODO(yanavlasov): make validation in HTTP/2 case stricter. |
212 | 211k | bool is_valid = true; |
213 | 3.01M | for (auto iter = header_key.begin(); iter != header_key.end() && is_valid; ++iter) { |
214 | 2.80M | is_valid &= testCharInTable(kGenericHeaderNameCharTable, *iter); |
215 | 2.80M | } |
216 | 211k | return is_valid; |
217 | 636k | } |
218 | | |
219 | 18.6k | bool HeaderUtility::headerNameContainsUnderscore(const absl::string_view header_name) { |
220 | 18.6k | return header_name.find('_') != absl::string_view::npos; |
221 | 18.6k | } |
222 | | |
223 | 359k | bool HeaderUtility::authorityIsValid(const absl::string_view header_value) { |
224 | 359k | if (Runtime::runtimeFeatureEnabled( |
225 | 359k | "envoy.reloadable_features.http2_validate_authority_with_quiche")) { |
226 | 359k | return http2::adapter::HeaderValidator::IsValidAuthority(header_value); |
227 | 359k | } else { |
228 | 0 | return nghttp2_check_authority(reinterpret_cast<const uint8_t*>(header_value.data()), |
229 | 0 | header_value.size()) != 0; |
230 | 0 | } |
231 | 359k | } |
232 | | |
233 | 33.5k | bool HeaderUtility::isSpecial1xx(const ResponseHeaderMap& response_headers) { |
234 | 33.5k | return response_headers.Status()->value() == "100" || |
235 | 33.5k | response_headers.Status()->value() == "102" || response_headers.Status()->value() == "103"; |
236 | 33.5k | } |
237 | | |
238 | 692k | bool HeaderUtility::isConnect(const RequestHeaderMap& headers) { |
239 | 692k | return headers.Method() && headers.Method()->value() == Http::Headers::get().MethodValues.Connect; |
240 | 692k | } |
241 | | |
242 | 79.0k | bool HeaderUtility::isConnectUdpRequest(const RequestHeaderMap& headers) { |
243 | 79.0k | return headers.Upgrade() && absl::EqualsIgnoreCase(headers.getUpgradeValue(), |
244 | 9.11k | Http::Headers::get().UpgradeValues.ConnectUdp); |
245 | 79.0k | } |
246 | | |
247 | 0 | bool HeaderUtility::isConnectUdpResponse(const ResponseHeaderMap& headers) { |
248 | | // In connect-udp case, Envoy will transform the H2 headers to H1 upgrade headers. |
249 | | // A valid response should have SwitchingProtocol status and connect-udp upgrade. |
250 | 0 | return headers.Upgrade() && Utility::getResponseStatus(headers) == 101 && |
251 | 0 | absl::EqualsIgnoreCase(headers.getUpgradeValue(), |
252 | 0 | Http::Headers::get().UpgradeValues.ConnectUdp); |
253 | 0 | } |
254 | | |
255 | | bool HeaderUtility::isConnectResponse(const RequestHeaderMap* request_headers, |
256 | 115k | const ResponseHeaderMap& response_headers) { |
257 | 115k | return request_headers && isConnect(*request_headers) && |
258 | 115k | static_cast<Http::Code>(Http::Utility::getResponseStatus(response_headers)) == |
259 | 215 | Http::Code::OK; |
260 | 115k | } |
261 | | |
262 | 7.90k | bool HeaderUtility::rewriteAuthorityForConnectUdp(RequestHeaderMap& headers) { |
263 | | // Per RFC 9298, the URI template must only contain ASCII characters in the range 0x21-0x7E. |
264 | 7.90k | absl::string_view path = headers.getPathValue(); |
265 | 6.04M | for (char c : path) { |
266 | 6.04M | unsigned char ascii_code = static_cast<unsigned char>(c); |
267 | 6.04M | if (ascii_code < 0x21 || ascii_code > 0x7e) { |
268 | 82 | ENVOY_LOG_MISC(warn, "CONNECT-UDP request with a bad character in the path {}", path); |
269 | 82 | return false; |
270 | 82 | } |
271 | 6.04M | } |
272 | | |
273 | | // Extract target host and port from path using default template. |
274 | 7.82k | if (!absl::StartsWith(path, "/.well-known/masque/udp/")) { |
275 | 2.42k | ENVOY_LOG_MISC(warn, "CONNECT-UDP request path is not a well-known URI: {}", path); |
276 | 2.42k | return false; |
277 | 2.42k | } |
278 | | |
279 | 5.39k | std::vector<absl::string_view> path_split = absl::StrSplit(path, '/'); |
280 | 5.39k | if (path_split.size() != 7 || path_split[4].empty() || path_split[5].empty() || |
281 | 5.39k | !path_split[6].empty()) { |
282 | 1.33k | ENVOY_LOG_MISC(warn, "CONNECT-UDP request with a malformed URI template in the path {}", path); |
283 | 1.33k | return false; |
284 | 1.33k | } |
285 | | |
286 | | // Utility::PercentEncoding::decode never returns an empty string if the input argument is not |
287 | | // empty. |
288 | 4.06k | std::string target_host = Utility::PercentEncoding::decode(path_split[4]); |
289 | | // Per RFC 9298, IPv6 Zone ID is not supported. |
290 | 4.06k | if (target_host.find('%') != std::string::npos) { |
291 | 3.59k | ENVOY_LOG_MISC(warn, "CONNECT-UDP request with a non-escpaed char (%) in the path {}", path); |
292 | 3.59k | return false; |
293 | 3.59k | } |
294 | 474 | std::string target_port = Utility::PercentEncoding::decode(path_split[5]); |
295 | | |
296 | | // If the host is an IPv6 address, surround the address with square brackets. |
297 | 474 | in6_addr sin6_addr; |
298 | 474 | bool is_ipv6 = (inet_pton(AF_INET6, target_host.c_str(), &sin6_addr) == 1); |
299 | 474 | std::string new_host = |
300 | 474 | absl::StrCat((is_ipv6 ? absl::StrCat("[", target_host, "]") : target_host), ":", target_port); |
301 | 474 | headers.setHost(new_host); |
302 | | |
303 | 474 | return true; |
304 | 4.06k | } |
305 | | |
306 | | #ifdef ENVOY_ENABLE_HTTP_DATAGRAMS |
307 | 3.69k | bool HeaderUtility::isCapsuleProtocol(const RequestOrResponseHeaderMap& headers) { |
308 | 3.69k | Http::HeaderMap::GetResult capsule_protocol = |
309 | 3.69k | headers.get(Envoy::Http::LowerCaseString("Capsule-Protocol")); |
310 | | // When there are multiple Capsule-Protocol header entries, it returns false. RFC 9297 specifies |
311 | | // that non-boolean value types must be ignored. If there are multiple header entries, the value |
312 | | // type becomes a List so the header field must be ignored. |
313 | 3.69k | if (capsule_protocol.size() != 1) { |
314 | 1.81k | return false; |
315 | 1.81k | } |
316 | | // Parses the header value and extracts the boolean value ignoring parameters. |
317 | 1.88k | absl::optional<quiche::structured_headers::ParameterizedItem> header_item = |
318 | 1.88k | quiche::structured_headers::ParseItem(capsule_protocol[0]->value().getStringView()); |
319 | 1.88k | return header_item && header_item->item.is_boolean() && header_item->item.GetBoolean(); |
320 | 3.69k | } |
321 | | #endif |
322 | | |
323 | 11.4k | bool HeaderUtility::requestShouldHaveNoBody(const RequestHeaderMap& headers) { |
324 | 11.4k | return (headers.Method() && |
325 | 11.4k | (headers.Method()->value() == Http::Headers::get().MethodValues.Get || |
326 | 11.4k | headers.Method()->value() == Http::Headers::get().MethodValues.Head || |
327 | 11.4k | headers.Method()->value() == Http::Headers::get().MethodValues.Delete || |
328 | 11.4k | headers.Method()->value() == Http::Headers::get().MethodValues.Trace || |
329 | 11.4k | headers.Method()->value() == Http::Headers::get().MethodValues.Connect)); |
330 | 11.4k | } |
331 | | |
332 | 3.36k | bool HeaderUtility::isEnvoyInternalRequest(const RequestHeaderMap& headers) { |
333 | 3.36k | const HeaderEntry* internal_request_header = headers.EnvoyInternalRequest(); |
334 | 3.36k | return internal_request_header != nullptr && |
335 | 3.36k | internal_request_header->value() == Headers::get().EnvoyInternalRequestValues.True; |
336 | 3.36k | } |
337 | | |
338 | 114 | void HeaderUtility::stripTrailingHostDot(RequestHeaderMap& headers) { |
339 | 114 | auto host = headers.getHostValue(); |
340 | | // If the host ends in a period, remove it. |
341 | 114 | auto dot_index = host.rfind('.'); |
342 | 114 | if (dot_index == std::string::npos) { |
343 | 16 | return; |
344 | 98 | } else if (dot_index == (host.size() - 1)) { |
345 | 95 | host.remove_suffix(1); |
346 | 95 | headers.setHost(host); |
347 | 95 | return; |
348 | 95 | } |
349 | | // If the dot is just before a colon, it must be preceding the port number. |
350 | | // IPv6 addresses may contain colons or dots, but the dot will never directly |
351 | | // precede the colon, so this check should be sufficient to detect a trailing port number. |
352 | 3 | if (host[dot_index + 1] == ':') { |
353 | 0 | headers.setHost(absl::StrCat(host.substr(0, dot_index), host.substr(dot_index + 1))); |
354 | 0 | } |
355 | 3 | } |
356 | | |
357 | 0 | bool HeaderUtility::hostHasPort(absl::string_view original_host) { |
358 | 0 | const absl::string_view::size_type port_start = getPortStart(original_host); |
359 | 0 | const absl::string_view port_str = original_host.substr(port_start + 1); |
360 | 0 | if (port_start == absl::string_view::npos) { |
361 | 0 | return false; |
362 | 0 | } |
363 | 0 | uint32_t port = 0; |
364 | 0 | if (!absl::SimpleAtoi(port_str, &port)) { |
365 | 0 | return false; |
366 | 0 | } |
367 | 0 | return true; |
368 | 0 | } |
369 | | |
370 | | absl::optional<uint32_t> HeaderUtility::stripPortFromHost(RequestHeaderMap& headers, |
371 | 142 | absl::optional<uint32_t> listener_port) { |
372 | 142 | const absl::string_view original_host = headers.getHostValue(); |
373 | 142 | const absl::string_view::size_type port_start = getPortStart(original_host); |
374 | 142 | if (port_start == absl::string_view::npos) { |
375 | 112 | return absl::nullopt; |
376 | 112 | } |
377 | 30 | const absl::string_view port_str = original_host.substr(port_start + 1); |
378 | 30 | uint32_t port = 0; |
379 | 30 | if (!absl::SimpleAtoi(port_str, &port)) { |
380 | 26 | return absl::nullopt; |
381 | 26 | } |
382 | 4 | if (listener_port.has_value() && port != listener_port) { |
383 | | // We would strip ports only if it is specified and they are the same, as local port of the |
384 | | // listener. |
385 | 0 | return absl::nullopt; |
386 | 0 | } |
387 | 4 | const absl::string_view host = original_host.substr(0, port_start); |
388 | 4 | headers.setHost(host); |
389 | 4 | return port; |
390 | 4 | } |
391 | | |
392 | 2.67k | absl::string_view::size_type HeaderUtility::getPortStart(absl::string_view host) { |
393 | 2.67k | const absl::string_view::size_type port_start = host.rfind(':'); |
394 | 2.67k | if (port_start == absl::string_view::npos) { |
395 | 2.59k | return absl::string_view::npos; |
396 | 2.59k | } |
397 | | // According to RFC3986 v6 address is always enclosed in "[]". section 3.2.2. |
398 | 87 | const auto v6_end_index = host.rfind(']'); |
399 | 87 | if (v6_end_index == absl::string_view::npos || v6_end_index < port_start) { |
400 | 76 | if ((port_start + 1) > host.size()) { |
401 | 0 | return absl::string_view::npos; |
402 | 0 | } |
403 | 76 | return port_start; |
404 | 76 | } |
405 | 11 | return absl::string_view::npos; |
406 | 87 | } |
407 | | |
408 | 576k | constexpr bool isInvalidToken(unsigned char c) { |
409 | 576k | if (c == '!' || c == '|' || c == '~' || c == '*' || c == '+' || c == '-' || c == '.' || |
410 | | // #, $, %, &, ' |
411 | 576k | (c >= '#' && c <= '\'') || |
412 | | // [0-9] |
413 | 576k | (c >= '0' && c <= '9') || |
414 | | // [A-Z] |
415 | 576k | (c >= 'A' && c <= 'Z') || |
416 | | // ^, _, `, [a-z] |
417 | 576k | (c >= '^' && c <= 'z')) { |
418 | 576k | return false; |
419 | 576k | } |
420 | 49 | return true; |
421 | 576k | } |
422 | | |
423 | | absl::optional<std::reference_wrapper<const absl::string_view>> |
424 | 188k | HeaderUtility::requestHeadersValid(const RequestHeaderMap& headers) { |
425 | | // Make sure the host is valid. |
426 | 188k | if (headers.Host() && !HeaderUtility::authorityIsValid(headers.Host()->value().getStringView())) { |
427 | 419 | return SharedResponseCodeDetails::get().InvalidAuthority; |
428 | 419 | } |
429 | 187k | if (headers.Method()) { |
430 | 187k | absl::string_view method = headers.Method()->value().getStringView(); |
431 | 187k | if (method.empty() || std::any_of(method.begin(), method.end(), isInvalidToken)) { |
432 | 72 | return SharedResponseCodeDetails::get().InvalidMethod; |
433 | 72 | } |
434 | 187k | } |
435 | 187k | if (headers.Scheme() && absl::StrContains(headers.Scheme()->value().getStringView(), ",")) { |
436 | 24 | return SharedResponseCodeDetails::get().InvalidScheme; |
437 | 24 | } |
438 | 187k | return absl::nullopt; |
439 | 187k | } |
440 | | |
441 | | bool HeaderUtility::shouldCloseConnection(Http::Protocol protocol, |
442 | 176k | const RequestOrResponseHeaderMap& headers) { |
443 | | // HTTP/1.0 defaults to single-use connections. Make sure the connection will be closed unless |
444 | | // Keep-Alive is present. |
445 | 176k | if (protocol == Protocol::Http10 && |
446 | 176k | (!headers.Connection() || |
447 | 209 | !Envoy::StringUtil::caseFindToken(headers.Connection()->value().getStringView(), ",", |
448 | 209 | Http::Headers::get().ConnectionValues.KeepAlive))) { |
449 | 209 | return true; |
450 | 209 | } |
451 | | |
452 | 176k | if (protocol == Protocol::Http11 && headers.Connection() && |
453 | 176k | Envoy::StringUtil::caseFindToken(headers.Connection()->value().getStringView(), ",", |
454 | 2.30k | Http::Headers::get().ConnectionValues.Close)) { |
455 | 26 | return true; |
456 | 26 | } |
457 | | |
458 | | // Note: Proxy-Connection is not a standard header, but is supported here |
459 | | // since it is supported by http-parser the underlying parser for http |
460 | | // requests. |
461 | 176k | if (protocol < Protocol::Http2 && headers.ProxyConnection() && |
462 | 176k | Envoy::StringUtil::caseFindToken(headers.ProxyConnection()->value().getStringView(), ",", |
463 | 1.10k | Http::Headers::get().ConnectionValues.Close)) { |
464 | 39 | return true; |
465 | 39 | } |
466 | 176k | return false; |
467 | 176k | } |
468 | | |
469 | 147k | Http::Status HeaderUtility::checkRequiredRequestHeaders(const Http::RequestHeaderMap& headers) { |
470 | 147k | if (!headers.Method()) { |
471 | 0 | return absl::InvalidArgumentError( |
472 | 0 | absl::StrCat("missing required header: ", Envoy::Http::Headers::get().Method.get())); |
473 | 0 | } |
474 | 147k | bool is_connect = Http::HeaderUtility::isConnect(headers); |
475 | 147k | if (is_connect) { |
476 | 4.89k | if (!headers.Host()) { |
477 | | // Host header must be present for CONNECT request. |
478 | 139 | return absl::InvalidArgumentError( |
479 | 139 | absl::StrCat("missing required header: ", Envoy::Http::Headers::get().Host.get())); |
480 | 139 | } |
481 | 4.75k | if (headers.Path() && !headers.Protocol()) { |
482 | | // Path and Protocol header should only be present for CONNECT for upgrade style CONNECT. |
483 | 100 | return absl::InvalidArgumentError( |
484 | 100 | absl::StrCat("missing required header: ", Envoy::Http::Headers::get().Protocol.get())); |
485 | 100 | } |
486 | 4.65k | if (!headers.Path() && headers.Protocol()) { |
487 | | // Path and Protocol header should only be present for CONNECT for upgrade style CONNECT. |
488 | 0 | return absl::InvalidArgumentError( |
489 | 0 | absl::StrCat("missing required header: ", Envoy::Http::Headers::get().Path.get())); |
490 | 0 | } |
491 | 142k | } else { |
492 | 142k | if (!headers.Path()) { |
493 | | // :path header must be present for non-CONNECT requests. |
494 | 59 | return absl::InvalidArgumentError( |
495 | 59 | absl::StrCat("missing required header: ", Envoy::Http::Headers::get().Path.get())); |
496 | 59 | } |
497 | 142k | } |
498 | 147k | return Http::okStatus(); |
499 | 147k | } |
500 | | |
501 | 125k | Http::Status HeaderUtility::checkValidRequestHeaders(const Http::RequestHeaderMap& headers) { |
502 | 125k | if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.validate_upstream_headers")) { |
503 | 0 | return Http::okStatus(); |
504 | 0 | } |
505 | | |
506 | 125k | const HeaderEntry* invalid_entry = nullptr; |
507 | 125k | bool invalid_key = false; |
508 | 586k | headers.iterate([&invalid_entry, &invalid_key](const HeaderEntry& header) -> HeaderMap::Iterate { |
509 | 586k | if (!HeaderUtility::headerNameIsValid(header.key().getStringView())) { |
510 | 13.2k | invalid_entry = &header; |
511 | 13.2k | invalid_key = true; |
512 | 13.2k | return HeaderMap::Iterate::Break; |
513 | 13.2k | } |
514 | | |
515 | 573k | if (!HeaderUtility::headerValueIsValid(header.value().getStringView())) { |
516 | 744 | invalid_entry = &header; |
517 | 744 | invalid_key = false; |
518 | 744 | return HeaderMap::Iterate::Break; |
519 | 744 | } |
520 | | |
521 | 572k | return HeaderMap::Iterate::Continue; |
522 | 573k | }); |
523 | | |
524 | 125k | if (invalid_entry) { |
525 | | // The header key may contain non-printable characters. Escape the key so that the error |
526 | | // details can be safely presented. |
527 | 13.9k | const absl::string_view key = invalid_entry->key().getStringView(); |
528 | 13.9k | uint64_t extra_length = JsonEscaper::extraSpace(key); |
529 | 13.9k | const std::string escaped_key = JsonEscaper::escapeString(key, extra_length); |
530 | | |
531 | 13.9k | return absl::InvalidArgumentError( |
532 | 13.9k | absl::StrCat("invalid header ", invalid_key ? "name: " : "value for: ", escaped_key)); |
533 | 13.9k | } |
534 | 111k | return Http::okStatus(); |
535 | 125k | } |
536 | | |
537 | 118k | Http::Status HeaderUtility::checkRequiredResponseHeaders(const Http::ResponseHeaderMap& headers) { |
538 | 118k | const absl::optional<uint64_t> status = Utility::getResponseStatusOrNullopt(headers); |
539 | 118k | if (!status.has_value()) { |
540 | 0 | return absl::InvalidArgumentError( |
541 | 0 | absl::StrCat("missing required header: ", Envoy::Http::Headers::get().Status.get())); |
542 | 0 | } |
543 | 118k | return Http::okStatus(); |
544 | 118k | } |
545 | | |
546 | 75.6k | bool HeaderUtility::isRemovableHeader(absl::string_view header) { |
547 | 75.6k | return (header.empty() || header[0] != ':') && |
548 | 75.6k | !absl::EqualsIgnoreCase(header, Headers::get().HostLegacy.get()); |
549 | 75.6k | } |
550 | | |
551 | 381k | bool HeaderUtility::isModifiableHeader(absl::string_view header) { |
552 | 381k | return (header.empty() || header[0] != ':') && |
553 | 381k | !absl::EqualsIgnoreCase(header, Headers::get().HostLegacy.get()); |
554 | 381k | } |
555 | | |
556 | | HeaderUtility::HeaderValidationResult HeaderUtility::checkHeaderNameForUnderscores( |
557 | | absl::string_view header_name, |
558 | | envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction |
559 | | headers_with_underscores_action, |
560 | 45.1k | HeaderValidatorStats& stats) { |
561 | 45.1k | if (headers_with_underscores_action == envoy::config::core::v3::HttpProtocolOptions::ALLOW || |
562 | 45.1k | !HeaderUtility::headerNameContainsUnderscore(header_name)) { |
563 | 45.1k | return HeaderValidationResult::ACCEPT; |
564 | 45.1k | } |
565 | 0 | if (headers_with_underscores_action == |
566 | 0 | envoy::config::core::v3::HttpProtocolOptions::DROP_HEADER) { |
567 | 0 | ENVOY_LOG_MISC(debug, "Dropping header with invalid characters in its name: {}", header_name); |
568 | 0 | stats.incDroppedHeadersWithUnderscores(); |
569 | 0 | return HeaderValidationResult::DROP; |
570 | 0 | } |
571 | 0 | ENVOY_LOG_MISC(debug, "Rejecting request due to header name with underscores: {}", header_name); |
572 | 0 | stats.incRequestsRejectedWithUnderscoresInHeaders(); |
573 | 0 | return HeaderUtility::HeaderValidationResult::REJECT; |
574 | 0 | } |
575 | | |
576 | | HeaderUtility::HeaderValidationResult |
577 | | HeaderUtility::validateContentLength(absl::string_view header_value, |
578 | | bool override_stream_error_on_invalid_http_message, |
579 | 421 | bool& should_close_connection, size_t& content_length_output) { |
580 | 421 | should_close_connection = false; |
581 | 421 | std::vector<absl::string_view> values = absl::StrSplit(header_value, ','); |
582 | 421 | absl::optional<uint64_t> content_length; |
583 | 421 | for (const absl::string_view& value : values) { |
584 | 421 | uint64_t new_value; |
585 | 421 | if (!absl::SimpleAtoi(value, &new_value) || |
586 | 421 | !std::all_of(value.begin(), value.end(), absl::ascii_isdigit)) { |
587 | 3 | ENVOY_LOG_MISC(debug, "Content length was either unparseable or negative"); |
588 | 3 | should_close_connection = !override_stream_error_on_invalid_http_message; |
589 | 3 | return HeaderValidationResult::REJECT; |
590 | 3 | } |
591 | 418 | if (!content_length.has_value()) { |
592 | 418 | content_length = new_value; |
593 | 418 | continue; |
594 | 418 | } |
595 | 0 | if (new_value != content_length.value()) { |
596 | 0 | ENVOY_LOG_MISC( |
597 | 0 | debug, |
598 | 0 | "Parsed content length {} is inconsistent with previously detected content length {}", |
599 | 0 | new_value, content_length.value()); |
600 | 0 | should_close_connection = !override_stream_error_on_invalid_http_message; |
601 | 0 | return HeaderValidationResult::REJECT; |
602 | 0 | } |
603 | 0 | } |
604 | 418 | content_length_output = content_length.value(); |
605 | 418 | return HeaderValidationResult::ACCEPT; |
606 | 421 | } |
607 | | |
608 | | std::vector<absl::string_view> |
609 | 0 | HeaderUtility::parseCommaDelimitedHeader(absl::string_view header_value) { |
610 | 0 | std::vector<absl::string_view> values; |
611 | 0 | for (absl::string_view s : absl::StrSplit(header_value, ',')) { |
612 | 0 | absl::string_view token = absl::StripAsciiWhitespace(s); |
613 | 0 | if (token.empty()) { |
614 | 0 | continue; |
615 | 0 | } |
616 | 0 | values.emplace_back(token); |
617 | 0 | } |
618 | 0 | return values; |
619 | 0 | } |
620 | | |
621 | 0 | absl::string_view HeaderUtility::getSemicolonDelimitedAttribute(absl::string_view value) { |
622 | 0 | return absl::StripAsciiWhitespace(StringUtil::cropRight(value, ";")); |
623 | 0 | } |
624 | | |
625 | | std::string HeaderUtility::addEncodingToAcceptEncoding(absl::string_view accept_encoding_header, |
626 | 0 | absl::string_view encoding) { |
627 | | // Append the content encoding only if it isn't already present in the |
628 | | // accept_encoding header. If it is present with a q-value ("gzip;q=0.3"), |
629 | | // remove the q-value to indicate that the content encoding setting that we |
630 | | // add has max priority (i.e. q-value 1.0). |
631 | 0 | std::vector<absl::string_view> newContentEncodings; |
632 | 0 | std::vector<absl::string_view> contentEncodings = |
633 | 0 | Http::HeaderUtility::parseCommaDelimitedHeader(accept_encoding_header); |
634 | 0 | for (absl::string_view contentEncoding : contentEncodings) { |
635 | 0 | absl::string_view strippedEncoding = |
636 | 0 | Http::HeaderUtility::getSemicolonDelimitedAttribute(contentEncoding); |
637 | 0 | if (strippedEncoding != encoding) { |
638 | | // Add back all content encodings back except for the content encoding that we want to |
639 | | // add. For example, if content encoding is "gzip", this filters out encodings "gzip" and |
640 | | // "gzip;q=0.6". |
641 | 0 | newContentEncodings.push_back(contentEncoding); |
642 | 0 | } |
643 | 0 | } |
644 | | // Finally add a single instance of our content encoding. |
645 | 0 | newContentEncodings.push_back(encoding); |
646 | 0 | return absl::StrJoin(newContentEncodings, ","); |
647 | 0 | } |
648 | | |
649 | 594 | bool HeaderUtility::isStandardConnectRequest(const Http::RequestHeaderMap& headers) { |
650 | 594 | return headers.method() == Http::Headers::get().MethodValues.Connect && |
651 | 594 | headers.getProtocolValue().empty(); |
652 | 594 | } |
653 | | |
654 | 0 | bool HeaderUtility::isExtendedH2ConnectRequest(const Http::RequestHeaderMap& headers) { |
655 | 0 | return headers.method() == Http::Headers::get().MethodValues.Connect && |
656 | 0 | !headers.getProtocolValue().empty(); |
657 | 0 | } |
658 | | |
659 | 135k | bool HeaderUtility::isPseudoHeader(absl::string_view header_name) { |
660 | 135k | return !header_name.empty() && header_name[0] == ':'; |
661 | 135k | } |
662 | | |
663 | | } // namespace Http |
664 | | } // namespace Envoy |