diff --git a/CODEOWNERS b/CODEOWNERS index 1edcf7c68c1ef..44c85131b8e51 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -23,6 +23,8 @@ extensions/filters/common/original_src @snowp @klarose /*/extensions/filters/network/rocketmq_proxy @aaron-ai @lizhanhui @lizan # thrift_proxy extension /*/extensions/filters/network/thrift_proxy @zuercher @rgs1 +# cdn_loop extension +/*/extensions/filters/http/cdn_loop @justin-mp @penguingao @alyssawilk # compressor used by http compression filters /*/extensions/filters/http/common/compressor @gsagula @rojkov @dio /*/extensions/filters/http/compressor @rojkov @dio diff --git a/source/extensions/filters/http/cdn_loop/BUILD b/source/extensions/filters/http/cdn_loop/BUILD new file mode 100644 index 0000000000000..ee566df7172dc --- /dev/null +++ b/source/extensions/filters/http/cdn_loop/BUILD @@ -0,0 +1,26 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "parser_lib", + srcs = ["parser.cc"], + hdrs = ["parser.h"], + deps = ["//source/common/common:statusor_lib"], +) + +envoy_cc_library( + name = "utils_lib", + srcs = ["utils.cc"], + hdrs = ["utils.h"], + deps = [ + ":parser_lib", + "//source/common/common:statusor_lib", + ], +) diff --git a/source/extensions/filters/http/cdn_loop/parser.cc b/source/extensions/filters/http/cdn_loop/parser.cc new file mode 100644 index 0000000000000..e686406c2fd33 --- /dev/null +++ b/source/extensions/filters/http/cdn_loop/parser.cc @@ -0,0 +1,381 @@ +#include "extensions/filters/http/cdn_loop/parser.h" + +#include "common/common/statusor.h" + +#include "absl/status/status.h" +#include "absl/strings/str_format.h" +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace CdnLoop { +namespace Parser { + +namespace { + +// RFC 5234 Appendix B.1 says: +// +// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z +constexpr bool isAlpha(char c) { + return ('\x41' <= c && c <= '\x5a') || ('\x61' <= c && c <= '\x7a'); +} + +// RFC 5234 Appendix B.1 says: +// +// DIGIT = %x30-39 ; 0-9 +constexpr bool isDigit(char c) { return '\x30' <= c && c <= '\x39'; } + +// RFC 2234 Section 6.1 defines HEXDIG as: +// +// HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" +// +// This rule allows lower case letters too in violation of the RFC since IPv6 +// addresses commonly contain lower-case hex digits. +constexpr bool isHexDigitCaseInsensitive(char c) { + return isDigit(c) || ('A' <= c && c <= 'F') || ('a' <= c && c <= 'f'); +} + +// RFC 7230 Section 3.2.6 defines obs-text as: +// +// obs-text = %x80-FF +constexpr bool isObsText(char c) { return 0x80 & c; } + +// RFC 7230 Section 3.2.6 defines qdtext as: +// +// qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text +constexpr bool isQdText(char c) { + return c == '\t' || c == ' ' || c == '\x21' || ('\x23' <= c && c <= '\x5B') || + ('\x5D' <= c && c <= '\x7E') || isObsText(c); +} + +// RFC 5234 Appendix B.1 says: +// +// VCHAR = %x21-7E +// ; visible (printing) characters +constexpr bool isVChar(char c) { return '\x21' <= c && c <= '\x7e'; } + +} // namespace + +ParseContext skipOptionalWhitespace(const ParseContext& input) { + ParseContext context = input; + while (!context.atEnd()) { + const char c = context.peek(); + if (!(c == ' ' || c == '\t')) { + break; + } + context.increment(); + } + return context; +} + +StatusOr parseQuotedPair(const ParseContext& input) { + ParseContext context = input; + if (context.atEnd()) { + return absl::InvalidArgumentError( + absl::StrFormat("expected backslash at position %d; found end-of-input", context.next())); + } + + if (context.peek() != '\\') { + return absl::InvalidArgumentError(absl::StrFormat( + "expected backslash at position %d; found '%c'", input.next(), context.peek())); + } + context.increment(); + + if (context.atEnd()) { + return absl::InvalidArgumentError(absl::StrFormat( + "expected escaped character at position %d; found end-of-input", context.next())); + } + + const char c = context.peek(); + if (!(c == '\t' || c == ' ' || isVChar(c) || isObsText(c))) { + return absl::InvalidArgumentError( + absl::StrFormat("expected escapable character at position %d; found '\\x%x'", input.next(), + context.peek())); + } + context.increment(); + + return context; +} + +StatusOr parseQuotedString(const ParseContext& input) { + ParseContext context = input; + + if (context.atEnd()) { + return absl::InvalidArgumentError(absl::StrFormat( + "expected opening '\"' at position %d; found end-of-input", context.next())); + } + + if (context.peek() != '"') { + return absl::InvalidArgumentError(absl::StrFormat( + "expected opening quote at position %d; found '%c'", context.next(), context.peek())); + } + context.increment(); + + while (!context.atEnd() && context.peek() != '"') { + if (isQdText(context.peek())) { + context.increment(); + continue; + } else if (context.peek() == '\\') { + if (StatusOr quoted_pair_context = parseQuotedPair(context); + !quoted_pair_context) { + return quoted_pair_context.status(); + } else { + context.setNext(*quoted_pair_context); + continue; + } + } else { + break; + } + } + + if (context.atEnd()) { + return absl::InvalidArgumentError(absl::StrFormat( + "expected closing quote at position %d; found end-of-input", context.next())); + } + + if (context.peek() != '"') { + return absl::InvalidArgumentError(absl::StrFormat( + "expected closing quote at position %d; found '%c'", input.next(), context.peek())); + } + context.increment(); + + return context; +} + +StatusOr parseToken(const ParseContext& input) { + ParseContext context = input; + while (!context.atEnd()) { + const char c = context.peek(); + // Put alphanumeric, -, and _ characters at the head of the list since + // they're likely to be used most often. + if (isAlpha(c) || isDigit(c) || c == '-' || c == '_' || c == '!' || c == '#' || c == '$' || + c == '%' || c == '&' || c == '\'' || c == '*' || c == '+' || c == '.' || c == '^' || + c == '`' || c == '|' || c == '~') { + context.increment(); + } else { + break; + } + } + if (context.next() == input.next()) { + if (context.atEnd()) { + return absl::InvalidArgumentError(absl::StrFormat( + "expected token starting at position %d; found end of input", input.next())); + } else { + return absl::InvalidArgumentError(absl::StrFormat( + "expected token starting at position %d; found '%c'", input.next(), context.peek())); + } + } + + return context; +} + +StatusOr parsePlausibleIpV6(const ParseContext& input) { + ParseContext context = input; + if (context.atEnd()) { + return absl::InvalidArgumentError(absl::StrFormat( + "expected IPv6 literal at position %d; found end-of-input", context.next())); + } + + if (context.peek() != '[') { + return absl::InvalidArgumentError(absl::StrFormat("expected opening '[' of IPv6 literal at " + "position %d; found '%c'", + context.next(), context.peek())); + } + context.increment(); + + while (true) { + if (context.atEnd()) { + break; + } + const char c = context.peek(); + if (!(isHexDigitCaseInsensitive(c) || c == ':' || c == '.')) { + break; + } + context.increment(); + } + + if (context.atEnd()) { + return absl::InvalidArgumentError( + absl::StrFormat("expected closing ']' of IPv6 literal at position %d " + "found end-of-input", + context.next())); + } + if (context.peek() != ']') { + return absl::InvalidArgumentError(absl::StrFormat("expected closing ']' of IPv6 literal at " + "position %d; found '%c'", + context.next(), context.peek())); + } + context.increment(); + + return context; +} + +StatusOr parseCdnId(const ParseContext& input) { + ParseContext context = input; + + if (context.atEnd()) { + return absl::InvalidArgumentError( + absl::StrFormat("expected cdn-id at position %d; found end-of-input", context.next())); + } + + // Optimization: dispatch on the next character to avoid the StrFormat in the + // error path of an IPv6 parser when the value has a token (and vice versa). + if (context.peek() == '[') { + if (StatusOr ipv6 = parsePlausibleIpV6(context); !ipv6) { + return ipv6.status(); + } else { + context.setNext(*ipv6); + } + } else { + if (StatusOr token = parseToken(context); !token) { + return token.status(); + } else { + context.setNext(*token); + } + } + + if (context.atEnd()) { + return ParsedCdnId(context, + context.value().substr(input.next(), context.next() - input.next())); + } + + if (context.peek() != ':') { + return ParsedCdnId(context, + context.value().substr(input.next(), context.next() - input.next())); + } + context.increment(); + + while (!context.atEnd()) { + if (isDigit(context.value()[context.next()])) { + context.increment(); + } else { + break; + } + } + + return ParsedCdnId(context, context.value().substr(input.next(), context.next() - input.next())); +} + +StatusOr parseParameter(const ParseContext& input) { + ParseContext context = input; + + if (StatusOr parsed_token = parseToken(context); !parsed_token) { + return parsed_token.status(); + } else { + context.setNext(*parsed_token); + } + + if (context.atEnd()) { + return absl::InvalidArgumentError( + absl::StrFormat("expected '=' at position %d; found end-of-input", context.next())); + } + + if (context.peek() != '=') { + return absl::InvalidArgumentError( + absl::StrFormat("expected '=' at position %d; found '%c'", context.next(), context.peek())); + } + context.increment(); + + if (context.atEnd()) { + return absl::InvalidArgumentError(absl::StrCat( + "expected token or quoted-string at position %d; found end-of-input", context.next())); + } + + // Optimization: dispatch on the next character to avoid the StrFormat in the + // error path of an quoted string parser when the next item is a token (and + // vice versa). + if (context.peek() == '"') { + if (StatusOr value_quote = parseQuotedString(context); !value_quote) { + return value_quote.status(); + } else { + return *value_quote; + } + } else { + if (StatusOr value_token = parseToken(context); !value_token) { + return value_token.status(); + } else { + return *value_token; + } + } +} + +StatusOr parseCdnInfo(const ParseContext& input) { + absl::string_view cdn_id; + ParseContext context = input; + if (StatusOr parsed_id = parseCdnId(input); !parsed_id) { + return parsed_id.status(); + } else { + context.setNext(parsed_id->context()); + cdn_id = parsed_id->cdnId(); + } + + context.setNext(skipOptionalWhitespace(context)); + + while (!context.atEnd()) { + if (context.peek() != ';') { + break; + } + context.increment(); + + context.setNext(skipOptionalWhitespace(context)); + + if (StatusOr parameter = parseParameter(context); !parameter) { + return parameter.status(); + } else { + context.setNext(*parameter); + } + + context.setNext(skipOptionalWhitespace(context)); + } + + return ParsedCdnInfo(context, cdn_id); +} + +StatusOr parseCdnInfoList(const ParseContext& input) { + std::vector cdn_infos; + ParseContext context = input; + + context.setNext(skipOptionalWhitespace(context)); + + while (!context.atEnd()) { + // Loop invariant: we're always at the beginning of a new element. + + if (context.peek() == ',') { + // Empty element case + context.increment(); + context.setNext(skipOptionalWhitespace(context)); + continue; + } + + if (StatusOr parsed_cdn_info = parseCdnInfo(context); !parsed_cdn_info) { + return parsed_cdn_info.status(); + } else { + cdn_infos.push_back(parsed_cdn_info->cdnId()); + context.setNext(parsed_cdn_info->context()); + } + + context.setNext(skipOptionalWhitespace(context)); + + if (context.atEnd()) { + break; + } + + if (context.peek() != ',') { + return absl::InvalidArgumentError(absl::StrFormat("expected ',' at position %d; found '%c'", + context.next(), context.peek())); + } else { + context.increment(); + } + + context.setNext(skipOptionalWhitespace(context)); + } + + return ParsedCdnInfoList(context, std::move(cdn_infos)); +} + +} // namespace Parser +} // namespace CdnLoop +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cdn_loop/parser.h b/source/extensions/filters/http/cdn_loop/parser.h new file mode 100644 index 0000000000000..5b9aad5175b26 --- /dev/null +++ b/source/extensions/filters/http/cdn_loop/parser.h @@ -0,0 +1,298 @@ +#pragma once + +#include + +#include "common/common/statusor.h" + +#include "absl/strings/str_join.h" +#include "absl/strings/string_view.h" + +// This file defines a parser for the CDN-Loop header value. +// +// RFC 8586 Section 2 defined the CDN-Loop header as: +// +// CDN-Loop = #cdn-info +// cdn-info = cdn-id *( OWS ";" OWS parameter ) +// cdn-id = ( uri-host [ ":" port ] ) / pseudonym +// pseudonym = token +// +// Each of those productions rely on definitions in RFC 3986, RFC 5234, RFC +// 7230, and RFC 7231. Their use is noted in the individual parse functions. +// +// The parser is a top-down combined parser and lexer that implements just +// enough of the RFC spec to make it possible count the number of times a +// particular CDN value appears. The main differences between the RFC's grammar +// and the parser defined here are: +// +// 1. the parser has a more lax interpretation of what's a valid uri-host. See +// ParseCdnId for details. +// +// 2. the parser allows leading and trailing whitespace around the header +// value. See ParseCdnInfoList for details. +// +// Each parse function takes as input a ParseContext that tells the +// function where to start. Parse functions that just need to parse a portion +// of the CDN-Loop header, but don't need to return a value, should return a +// ParseContext pointing to the next character to parse. Parse functions that +// need to return a value should return something that contains a ParseContext. +// +// Parse functions that can fail (most of them!) wrap their return value in an +// Envoy::StatusOr. +// +// In the interest of performance, this parser works with string_views and +// references instead of copying std::strings. The string_view passed into the +// ParseContext of a parse function must outlive the return value of the +// function. + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace CdnLoop { +namespace Parser { + +// A ParseContext contains the state of the recursive descent parser and some +// helper methods. +class ParseContext { +public: + ParseContext(absl::string_view value) : value_(value), next_(0) {} + ParseContext(absl::string_view value, absl::string_view::size_type next) + : value_(value), next_(next) {} + + // Returns true if we have reached the end of value. + constexpr bool atEnd() const { return value_.length() <= next_; } + + // Returns the value we're parsing + constexpr absl::string_view value() const { return value_; } + + // Returns the position of the next character to process. + constexpr absl::string_view::size_type next() const { return next_; } + + // Returns the character at next. + // + // REQUIRES: !at_end() + constexpr char peek() const { return value_[next_]; } + + // Moves to the next character. + constexpr void increment() { ++next_; } + + // Sets next from another context. + constexpr void setNext(const ParseContext& other) { next_ = other.next_; } + + constexpr bool operator==(const ParseContext& other) const { + return value_ == other.value_ && next_ == other.next_; + } + constexpr bool operator!=(const ParseContext& other) const { return !(*this == other); } + + friend std::ostream& operator<<(std::ostream& os, ParseContext arg) { + return os << "ParseContext{next=" << arg.next_ << "}"; + } + +private: + // The item we're parsing. + const absl::string_view value_; + + // A pointer to the next value we should parse. + absl::string_view::size_type next_; +}; + +// A ParsedCdnId holds an extracted CDN-Loop cdn-id. +class ParsedCdnId { +public: + ParsedCdnId(ParseContext context, absl::string_view cdn_id) + : context_(context), cdn_id_(cdn_id) {} + + ParseContext context() const { return context_; } + + absl::string_view cdnId() const { return cdn_id_; } + + constexpr bool operator==(const ParsedCdnId& other) const { + return context_ == other.context_ && cdn_id_ == other.cdn_id_; + } + constexpr bool operator!=(const ParsedCdnId& other) const { return !(*this == other); } + + friend std::ostream& operator<<(std::ostream& os, ParsedCdnId arg) { + return os << "ParsedCdnId{context=" << arg.context_ << ", cdn_id=" << arg.cdn_id_ << "}"; + } + +private: + ParseContext context_; + absl::string_view cdn_id_; +}; + +// A ParsedCdnInfo holds the extracted cdn-id after parsing an entire cdn-info. +struct ParsedCdnInfo { + ParsedCdnInfo(ParseContext context, absl::string_view cdn_id) + : context_(context), cdn_id_(cdn_id) {} + + ParseContext context() const { return context_; } + + absl::string_view cdnId() const { return cdn_id_; } + + constexpr bool operator==(const ParsedCdnInfo& other) const { + return context_ == other.context_ && cdn_id_ == other.cdn_id_; + } + constexpr bool operator!=(const ParsedCdnInfo& other) const { return !(*this == other); } + + friend std::ostream& operator<<(std::ostream& os, ParsedCdnInfo arg) { + return os << "ParsedCdnInfo{context=" << arg.context_ << ", cdn_id=" << arg.cdn_id_ << "}"; + } + +private: + ParseContext context_; + absl::string_view cdn_id_; +}; + +// A ParsedCdnInfoList contains list of cdn-ids after parsing the entire +// CDN-Loop production. +struct ParsedCdnInfoList { + ParsedCdnInfoList(ParseContext context, std::vector cdn_ids) + : context_(context), cdn_ids_(std::move(cdn_ids)) {} + + constexpr const std::vector& cdnIds() { return cdn_ids_; } + + constexpr bool operator==(const ParsedCdnInfoList& other) const { + return context_ == other.context_ && cdn_ids_ == other.cdn_ids_; + } + constexpr bool operator!=(const ParsedCdnInfoList& other) const { return !(*this == other); } + + friend std::ostream& operator<<(std::ostream& os, ParsedCdnInfoList arg) { + return os << "ParsedCdnInfoList{context=" << arg.context_ << ", cdn_ids=[" + << absl::StrJoin(arg.cdn_ids_, ", ") << "]}"; + } + +private: + ParseContext context_; + std::vector cdn_ids_; +}; + +// Skips optional whitespace according to RFC 7230 Section 3.2.3. +// +// OWS = *( SP / HTAB ) +// +// Since this is completely optional, there's no way this call can fail. +ParseContext skipOptionalWhitespace(const ParseContext& input); + +// Parses a quoted-pair according to RFC 7230 Section 3.2.6. +// +// quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) +StatusOr parseQuotedPair(const ParseContext& input); + +// Parses a quoted-string according to RFC 7230 Section 3.2.6. +// +// quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE +// qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text +// obs-text = %x80-FF +// +// quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) +StatusOr parseQuotedString(const ParseContext& input); + +// Parses a token according to RFC 7320 Section 3.2.6. +// +// token = 1*tchar +// +// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" +// / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" +// / DIGIT / ALPHA +// ; any VCHAR, except delimiters +// +// According to RFC 5234 Appendix B.1: +// +// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z +// +// DIGIT = %x30-39 +StatusOr parseToken(const ParseContext& input); + +// Parses something that looks like an IPv6 address literal. +// +// A proper IPv6 address literal is defined in RFC 3986, Section 3.2.2 as part +// of the host rule. We're going to allow something simpler: +// +// plausible-ipv6 = "[" *( HEXDIGIT | "." | ":" ) "] +// HEXDIGIT = DIGIT | %x41-46 | %x61-66 ; 0-9 | A-F | a-f +// +// Compared to the real rule, our rule: +// +// - allows lower-case hex digits +// - allows address sections with more than 4 hex digits in a row +// - allows embedded IPv4 addresses multiple times rather than just at the end. +StatusOr parsePlausibleIpV6(const ParseContext& input); + +// Parses a cdn-id in a lax way. +// +// According to to RFC 8586 Section 2, the cdn-id is: +// +// cdn-id = ( uri-host [ ":" port ] ) / pseudonym +// pseudonym = token +// +// The uri-host portion of the cdn-id is the "host" rule from RFC 3986 Section +// 3.2.2. Parsing the host rule is remarkably difficult because the host rule +// tries to parse exactly valid IP addresses (e.g., disallowing values greater +// than 255 in an IPv4 address or only allowing one instance of "::" in IPv6 +// addresses) and needs to deal with % escaping in names. +// +// Worse, the uri-host reg-name rule admits ',' and ';' as members of sub-delim +// rule, making parsing ambiguous in some cases! RFC 3986 does this in order to +// be "future-proof" for naming schemes we haven't dreamed up yet. RFC 8586 +// says that if a CDN uses a uri-host as its cdn-id, the uri-host must be a +// "hostname under its control". The only global naming system we have is DNS, +// so the only really valid reg-name an Internet-facing Envoy should see is a +// DNS name. +// +// Luckily, the token rule more or less covers the uri-host rule for DNS names +// and for IPv4 addresses. We just a new rule to parse IPv6 addresses. See +// ParsePlausibleIpV6 for the rule we'll follow. +// +// The definition of port comes from RFC 3986 Section +// 3.2.3 as: +// +// port = *DIGIT +// +// In other words, any number of digits is allowed. +// +// In all, this function will parse cdn-id as: +// +// cdn-id = ( plausible-ipv6-address / token ) [ ":" *DIGIT ] +StatusOr parseCdnId(const ParseContext& input); + +// Parses a parameter according RFC 7231 Appendix D. +// +// parameter = token "=" ( token / quoted-string ) +StatusOr parseParameter(const ParseContext& input); + +// Parses a cdn-info according to RFC 8586 Section 2. +// +// cdn-info = cdn-id *( OWS ";" OWS parameter ) +StatusOr parseCdnInfo(const ParseContext& input); + +// Parses the top-level cdn-info according to RFC 8586 Section 2. +// +// CDN-Loop = #cdn-info +// +// The # rule is defined by RFC 7230 Section 7. The # is different for senders +// and recipients. We're a recipient, so: +// +// For compatibility with legacy list rules, a recipient MUST parse and +// ignore a reasonable number of empty list elements: enough to handle +// common mistakes by senders that merge values, but not so much that +// they could be used as a denial-of-service mechanism. In other words, +// a recipient MUST accept lists that satisfy the following syntax: +// +// #element => [ ( "," / element ) *( OWS "," [ OWS element ] ) ] +// +// 1#element => *( "," OWS ) element *( OWS "," [ OWS element ] ) +// +// Empty elements do not contribute to the count of elements present. +// +// Since #cdn-info uses the #element form, we have to parse (but not count) +// blank entries. +// +// In a divergence with the RFC's grammar, this function will also ignore +// leading and trailing OWS. This function expects to consume the entire input +// and will return an error if there is something it cannot parse. +StatusOr parseCdnInfoList(const ParseContext& input); + +} // namespace Parser +} // namespace CdnLoop +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cdn_loop/utils.cc b/source/extensions/filters/http/cdn_loop/utils.cc new file mode 100644 index 0000000000000..2ea1a6a1945d8 --- /dev/null +++ b/source/extensions/filters/http/cdn_loop/utils.cc @@ -0,0 +1,32 @@ +#include "extensions/filters/http/cdn_loop/utils.h" + +#include + +#include "common/common/statusor.h" + +#include "extensions/filters/http/cdn_loop/parser.h" + +#include "absl/status/status.h" +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace CdnLoop { + +StatusOr countCdnLoopOccurrences(absl::string_view header, absl::string_view cdn_id) { + if (cdn_id.empty()) { + return absl::InvalidArgumentError("cdn_id cannot be empty"); + } + + if (absl::StatusOr parsed = Parser::parseCdnInfoList(header); parsed) { + return std::count(parsed->cdnIds().begin(), parsed->cdnIds().end(), cdn_id); + } else { + return parsed.status(); + } +} + +} // namespace CdnLoop +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/cdn_loop/utils.h b/source/extensions/filters/http/cdn_loop/utils.h new file mode 100644 index 0000000000000..ba486edadc160 --- /dev/null +++ b/source/extensions/filters/http/cdn_loop/utils.h @@ -0,0 +1,24 @@ +#pragma once + +#include "common/common/statusor.h" + +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace CdnLoop { + +// Count the number of times cdn_id appears as a cdn-id element in header. +// +// According to RFC 8586, a cdn-id is either a uri-host[:port] or a pseudonym. +// In either case, cdn_id must be at least one character long. +// +// If the header is unparseable or if cdn_id is the empty string, this function +// will return an InvalidArgument status. +StatusOr countCdnLoopOccurrences(absl::string_view header, absl::string_view cdn_id); + +} // namespace CdnLoop +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/cdn_loop/BUILD b/test/extensions/filters/http/cdn_loop/BUILD new file mode 100644 index 0000000000000..0f71bb670c4b5 --- /dev/null +++ b/test/extensions/filters/http/cdn_loop/BUILD @@ -0,0 +1,27 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_test( + name = "parser_test", + srcs = ["parser_test.cc"], + deps = [ + "//source/extensions/filters/http/cdn_loop:parser_lib", + "//test/test_common:status_utility_lib", + ], +) + +envoy_cc_test( + name = "utils_test", + srcs = ["utils_test.cc"], + deps = [ + "//source/extensions/filters/http/cdn_loop:utils_lib", + "//test/test_common:status_utility_lib", + ], +) diff --git a/test/extensions/filters/http/cdn_loop/parser_test.cc b/test/extensions/filters/http/cdn_loop/parser_test.cc new file mode 100644 index 0000000000000..cdd43ebfa224c --- /dev/null +++ b/test/extensions/filters/http/cdn_loop/parser_test.cc @@ -0,0 +1,515 @@ +#include + +#include "extensions/filters/http/cdn_loop/parser.h" + +#include "test/test_common/status_utility.h" + +#include "absl/status/status.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace CdnLoop { +namespace Parser { +namespace { + +using ::Envoy::StatusHelpers::IsOkAndHolds; +using ::Envoy::StatusHelpers::StatusIs; + +TEST(ParseContextOstreamTest, Works) { + std::ostringstream out; + ParseContext context("foo", 3); + out << context; + EXPECT_EQ(out.str(), "ParseContext{next=3}"); +} + +TEST(ParsedCdnIdOstreamTest, Works) { + std::ostringstream out; + ParsedCdnId cdnId(ParseContext("foo", 3), "foo"); + out << cdnId; + EXPECT_EQ(out.str(), "ParsedCdnId{context=ParseContext{next=3}, cdn_id=foo}"); +} + +TEST(ParsedCdnInfoOstreamTest, Works) { + std::ostringstream out; + ParsedCdnInfo cdnId(ParseContext("foo", 3), "foo"); + out << cdnId; + EXPECT_EQ(out.str(), "ParsedCdnInfo{context=ParseContext{next=3}, cdn_id=foo}"); +} + +TEST(ParsedCdnInfoListOstreamTest, Works) { + std::ostringstream out; + ParsedCdnInfoList cdnId(ParseContext("foo", 3), {"foo"}); + out << cdnId; + EXPECT_EQ(out.str(), "ParsedCdnInfoList{context=ParseContext{next=3}, cdn_ids=[foo]}"); +} + +TEST(SkipOptionalWhitespaceTest, TestEmpty) { + const std::string value = ""; + ParseContext input(value); + EXPECT_EQ(skipOptionalWhitespace(input), (ParseContext(value, 0))); +} + +TEST(SkipOptionalWhitespaceTest, TestSpace) { + const std::string value = " "; + ParseContext input(value); + EXPECT_EQ(skipOptionalWhitespace(input), (ParseContext(value, 1))); +} + +TEST(SkipOptionalWhitespaceTest, TestTab) { + const std::string value = "\t"; + ParseContext input(value); + EXPECT_EQ(skipOptionalWhitespace(input), (ParseContext(value, 1))); +} + +TEST(SkipOptionalWhitespaceTest, TestLots) { + const std::string value = " \t \t "; + ParseContext input(value); + EXPECT_EQ(skipOptionalWhitespace(input), (ParseContext(value, 7))); +} + +TEST(SkipOptionalWhitespaceTest, NoWhitespace) { + const std::string value = "c"; + ParseContext input(value); + EXPECT_EQ(skipOptionalWhitespace(input), (ParseContext(value, 0))); +} + +TEST(SkipOptionalWhitespaceTest, StopsAtNonWhitespace) { + const std::string value = " c"; + ParseContext input(value); + EXPECT_EQ(skipOptionalWhitespace(input), (ParseContext(value, 2))); +} + +TEST(ParseQuotedPairTest, Simple) { + const std::string value = R"(\a)"; + ParseContext input(value); + EXPECT_THAT(parseQuotedPair(input), IsOkAndHolds(ParseContext(value, 2))); +} + +TEST(ParseQuotedPairTest, EndOfInput) { + const std::string value = ""; + ParseContext input(value); + EXPECT_THAT(parseQuotedPair(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseQuotedPairTest, MissingQuotable) { + const std::string value = R"(\)"; + ParseContext input(value); + EXPECT_THAT(parseQuotedPair(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseQuotedPairTest, BadQuotable) { + const std::string value = "\\\x1f"; + ParseContext input(value); + EXPECT_THAT(parseQuotedPair(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseQuotedPairTest, MissingBackslash) { + const std::string value = R"(a)"; + ParseContext input(value); + EXPECT_THAT(parseQuotedPair(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseQuotedStringTest, Simple) { + const std::string value = "\"abcd\""; + ParseContext input(value); + EXPECT_THAT(parseQuotedString(input), IsOkAndHolds(ParseContext(value, 6))); +} + +TEST(ParseQuotedStringTest, QdStringEdgeCases) { + const std::string value = "\"\t \x21\x23\x5b\x5d\x7e\x80\xff\""; + ParseContext input(value); + EXPECT_THAT(parseQuotedString(input), IsOkAndHolds(ParseContext(value, 11))); +} + +TEST(ParseQuotedStringTest, QuotedPair) { + const std::string value = "\"\\\"\""; + ParseContext input(value); + EXPECT_THAT(parseQuotedString(input), IsOkAndHolds(ParseContext(value, 4))); +} + +TEST(ParseQuotedStringTest, NoStartQuote) { + const std::string value = "foo"; + ParseContext input(value); + EXPECT_THAT(parseQuotedString(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseQuotedStringTest, NoEndQuote) { + const std::string value = "\"missing-final-dquote"; + ParseContext input(value); + EXPECT_THAT(parseQuotedString(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseQuotedStringTest, EmptyInput) { + const std::string value = ""; + ParseContext input(value); + EXPECT_THAT(parseQuotedString(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseQuotedStringTest, NonVisualChar) { + const std::string value = "\"\x1f\""; + ParseContext input(value); + EXPECT_THAT(parseQuotedString(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseQuotedStringTest, QuotedPairEdgeCases) { + const std::string value = "\"\\"; + ParseContext input(value); + EXPECT_THAT(parseQuotedString(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseTokenTest, AllValues) { + const std::string value = "!#$%&'*+-.^_`|~09azAZ"; + ParseContext input(value); + EXPECT_THAT(parseToken(input), IsOkAndHolds(ParseContext(value, 21))); +} + +TEST(ParseTokenTest, TwoTokens) { + const std::string value = "token1 token2"; + { + ParseContext input(value); + EXPECT_THAT(parseToken(input), IsOkAndHolds(ParseContext(value, 6))); + } + { + ParseContext input(value, 6); + EXPECT_THAT(parseToken(input), StatusIs(absl::StatusCode::kInvalidArgument)); + } + { + ParseContext input(value, 7); + EXPECT_THAT(parseToken(input), IsOkAndHolds(ParseContext(value, 13))); + } +} + +TEST(ParseTokenTest, ParseEmpty) { + const std::string value = ""; + ParseContext input(value); + EXPECT_THAT(parseToken(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParsePlausibleIpV6, Example) { + const std::string value = "[2001:DB8::1]"; + ParseContext input(value); + EXPECT_THAT(parsePlausibleIpV6(input), IsOkAndHolds(ParseContext(value, 13))); +} + +TEST(ParsePlausibleIpV6, ExampleLowerCase) { + const std::string value = "[2001:db8::1]"; + ParseContext input(value); + EXPECT_THAT(parsePlausibleIpV6(input), IsOkAndHolds(ParseContext(value, 13))); +} + +TEST(ParsePlausibleIpV6, ExampleIpV4) { + const std::string value = "[2001:db8::192.0.2.0]"; + ParseContext input(value); + EXPECT_THAT(parsePlausibleIpV6(input), IsOkAndHolds(ParseContext(value, 21))); +} + +TEST(ParsePlausibleIpV6, AllHexValues) { + const std::string value = "[1234:5678:90aA:bBcC:dDeE:fF00]"; + ParseContext input(value); + EXPECT_THAT(parsePlausibleIpV6(input), IsOkAndHolds(ParseContext(value, 31))); +} + +TEST(ParsePlausibleIpV6, EmptyInput) { + const std::string value = ""; + ParseContext input(value); + EXPECT_THAT(parsePlausibleIpV6(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParsePlausibleIpV6, BadStartDelimiter) { + const std::string value = "{2001:DB8::1}"; + ParseContext input(value); + EXPECT_THAT(parsePlausibleIpV6(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParsePlausibleIpV6, BadCharacter) { + const std::string value = "[hello]"; + ParseContext input(value); + EXPECT_THAT(parsePlausibleIpV6(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParsePlausibleIpV6, BadEndDelimiter) { + const std::string value = "[2001:DB8::1}"; + ParseContext input(value); + EXPECT_THAT(parsePlausibleIpV6(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParsePlausibleIpV6, EndBeforeDelimiter) { + const std::string value = "[2001:DB8::1"; + ParseContext input(value); + EXPECT_THAT(parsePlausibleIpV6(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseCdnIdTest, Simple) { + const std::string value = "name"; + ParseContext input(value); + EXPECT_THAT(parseCdnId(input), IsOkAndHolds(ParsedCdnId(ParseContext(value, 4), "name"))); +} + +TEST(ParseCdnIdTest, SecondInSeries) { + // Make sure that absl::string_view::substr is called with (start, end) not + // (start, len) + const std::string value = "cdn1, cdn2, cdn3"; + ParseContext input(value, 6); + EXPECT_THAT(parseCdnId(input), IsOkAndHolds(ParsedCdnId(ParseContext(value, 10), "cdn2"))); +} + +TEST(ParseCdnIdTest, Empty) { + const std::string value = ""; + ParseContext input(value); + EXPECT_THAT(parseCdnId(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseCdnIdTest, NotValidTokenOrUri) { + const std::string value = ","; + ParseContext input(value); + EXPECT_THAT(parseCdnId(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseCdnIdTest, InvalidIpV6) { + const std::string value = "[2001::"; + ParseContext input(value); + EXPECT_THAT(parseCdnId(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseCdnIdTest, InvalidPortNumberStopsParse) { + const std::string value = "host:13z"; + ParseContext input(value); + EXPECT_THAT(parseCdnId(input), IsOkAndHolds(ParsedCdnId(ParseContext(value, 7), "host:13"))); +} + +TEST(ParseCdnIdTest, UriHostName) { + const std::string value = "www.example.com"; + ParseContext input(value); + EXPECT_THAT(parseCdnId(input), + IsOkAndHolds(ParsedCdnId(ParseContext(value, 15), "www.example.com"))); +} + +TEST(ParseCdnIdTest, UriHostPercentEncoded) { + const std::string value = "%ba%ba.example.com"; + ParseContext input(value); + EXPECT_THAT(parseCdnId(input), + IsOkAndHolds(ParsedCdnId(ParseContext(value, 18), "%ba%ba.example.com"))); +} + +TEST(ParseCdnIdTest, UriHostNamePort) { + const std::string value = "www.example.com:443"; + ParseContext input(value); + EXPECT_THAT(parseCdnId(input), + IsOkAndHolds(ParsedCdnId(ParseContext(value, 19), "www.example.com:443"))); +} + +TEST(ParseCdnIdTest, UriHostNameBlankPort) { + const std::string value = "www.example.com:"; + ParseContext input(value); + EXPECT_THAT(parseCdnId(input), + IsOkAndHolds(ParsedCdnId(ParseContext(value, 16), "www.example.com:"))); +} + +TEST(ParseCdnIdTest, UriHostIpV4) { + const std::string value = "192.0.2.0"; + ParseContext input(value); + EXPECT_THAT(parseCdnId(input), IsOkAndHolds(ParsedCdnId(ParseContext(value, 9), "192.0.2.0"))); +} + +TEST(ParseCdnIdTest, UriHostIpV4Port) { + const std::string value = "192.0.2.0:443"; + ParseContext input(value); + EXPECT_THAT(parseCdnId(input), + IsOkAndHolds(ParsedCdnId(ParseContext(value, 13), "192.0.2.0:443"))); +} + +TEST(ParseCdnIdTest, UriHostIpV4BlankPort) { + const std::string value = "192.0.2.0:"; + ParseContext input(value); + EXPECT_THAT(parseCdnId(input), IsOkAndHolds(ParsedCdnId(ParseContext(value, 10), "192.0.2.0:"))); +} + +TEST(ParseCdnIdTest, UriHostIpV6) { + const std::string value = "[2001:DB8::1]"; + ParseContext input(value); + EXPECT_THAT(parseCdnId(input), + IsOkAndHolds(ParsedCdnId(ParseContext(value, 13), "[2001:DB8::1]"))); +} + +TEST(ParseCdnIdTest, UriHostIpV6Port) { + const std::string value = "[2001:DB8::1]:443"; + ParseContext input(value); + EXPECT_THAT(parseCdnId(input), + IsOkAndHolds(ParsedCdnId(ParseContext(value, 17), "[2001:DB8::1]:443"))); +} + +TEST(ParseCdnIdTest, UriHostIpV6BlankPort) { + const std::string value = "[2001:DB8::1]:"; + ParseContext input(value); + EXPECT_THAT(parseCdnId(input), + IsOkAndHolds(ParsedCdnId(ParseContext(value, 14), "[2001:DB8::1]:"))); +} + +TEST(ParseParameterTest, SimpleTokenValue) { + const std::string value = "a=b"; + ParseContext input(value); + EXPECT_THAT(parseParameter(input), IsOkAndHolds(ParseContext(value, 3))); +} + +TEST(ParseParameterTest, SimpleQuotedValue) { + const std::string value = "a=\"b\""; + ParseContext input(value); + EXPECT_THAT(parseParameter(input), IsOkAndHolds(ParseContext(value, 5))); +} + +TEST(ParseParameterTest, EndOfInputBeforeEquals) { + const std::string value = "a"; + ParseContext input(value); + EXPECT_THAT(parseParameter(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseParameterTest, EndOfInputAfterEquals) { + const std::string value = "a="; + ParseContext input(value); + EXPECT_THAT(parseParameter(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseParameterTest, MissingEquals) { + const std::string value = "a,"; + ParseContext input(value); + EXPECT_THAT(parseParameter(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseParameterTest, ValueNotToken) { + const std::string value = "a=,"; + ParseContext input(value); + EXPECT_THAT(parseParameter(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseParameterTest, ValueNotQuotedString) { + const std::string value = "a=\""; + ParseContext input(value); + EXPECT_THAT(parseParameter(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseCdnInfoTest, Simple) { + const std::string value = "name"; + ParseContext input(value); + EXPECT_THAT(parseCdnInfo(input), IsOkAndHolds(ParsedCdnInfo(ParseContext(value, 4), "name"))); +} + +TEST(ParseCdnInfoTest, Empty) { + const std::string value = ""; + ParseContext input(value); + EXPECT_THAT(parseCdnInfo(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseCdnInfoTest, NotValidTokenOrUri) { + const std::string value = ","; + ParseContext input(value); + EXPECT_THAT(parseCdnInfo(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseCdnInfoTest, SingleParameter) { + const std::string value = "name;a=b"; + ParseContext input(value); + EXPECT_THAT(parseCdnInfo(input), IsOkAndHolds(ParsedCdnInfo(ParseContext(value, 8), "name"))); +} + +TEST(ParseCdnInfoTest, SingleParameterExtraWhitespace) { + const std::string value = "name ; a=b "; + ParseContext input(value); + EXPECT_THAT(parseCdnInfo(input), IsOkAndHolds(ParsedCdnInfo(ParseContext(value, 12), "name"))); +} + +TEST(ParseCdnInfoTest, MultipleParametersWithWhitespace) { + const std::string value = "name ; a=b ; c=\"d\" ; e=\";\" "; + ParseContext input(value); + EXPECT_THAT(parseCdnInfo(input), IsOkAndHolds(ParsedCdnInfo(ParseContext(value, 27), "name"))); +} + +TEST(ParseCdnInfoTest, MissingParameter) { + const std::string value = "name ; "; + ParseContext input(value); + EXPECT_THAT(parseCdnInfo(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseCdnInfoTest, InvalidParameter) { + const std::string value = "name ; a= "; + ParseContext input(value); + EXPECT_THAT(parseCdnInfo(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseCdnInfoListTest, Simple) { + const std::string value = "cdn1, cdn2, cdn3"; + ParseContext input(value); + EXPECT_THAT(parseCdnInfoList(input), + IsOkAndHolds(ParsedCdnInfoList(ParseContext(value, 16), {"cdn1", "cdn2", "cdn3"}))); +} + +TEST(ParseCdnInfoListTest, ExtraWhitespace) { + const std::string value = " \t cdn1 \t , cdn2 \t , \t cdn3 "; + ParseContext input(value); + EXPECT_THAT(parseCdnInfoList(input), + IsOkAndHolds(ParsedCdnInfoList(ParseContext(value, 33), {"cdn1", "cdn2", "cdn3"}))); +} + +TEST(ParseCdnInfoListTest, InvalidParseNoComma) { + const std::string value = "cdn1 cdn2"; + ParseContext input(value); + EXPECT_THAT(parseCdnInfoList(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseCdnInfoListTest, InvalidCdnId) { + const std::string value = "[bad"; + ParseContext input(value); + EXPECT_THAT(parseCdnInfoList(input), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(ParseCdnInfoListTest, Rfc7230Section7Tests) { + // These are the examples from https://tools.ietf.org/html/rfc7230#section-7 + { + const std::string value = "foo,bar"; + ParseContext input(value); + EXPECT_THAT(parseCdnInfoList(input), + IsOkAndHolds(ParsedCdnInfoList(ParseContext(value, 7), {"foo", "bar"}))); + } + { + const std::string value = "foo ,bar,"; + ParseContext input(value); + EXPECT_THAT(parseCdnInfoList(input), + IsOkAndHolds(ParsedCdnInfoList(ParseContext(value, 9), {"foo", "bar"}))); + } + { + const std::string value = "foo , ,bar,charlie "; + ParseContext input(value); + EXPECT_THAT(parseCdnInfoList(input), IsOkAndHolds(ParsedCdnInfoList( + ParseContext(value, 21), {"foo", "bar", "charlie"}))); + } + // The following tests are allowed in the #cdn-info rule because it doesn't + // require a single element. + { + const std::string value = ""; + ParseContext input(value); + + EXPECT_THAT(parseCdnInfoList(input), + IsOkAndHolds(ParsedCdnInfoList(ParseContext(value, 0), {}))); + } + { + const std::string value = ","; + ParseContext input(value); + EXPECT_THAT(parseCdnInfoList(input), + IsOkAndHolds(ParsedCdnInfoList(ParseContext(value, 1), {}))); + } + { + const std::string value = ", ,"; + ParseContext input(value); + EXPECT_THAT(parseCdnInfoList(input), + IsOkAndHolds(ParsedCdnInfoList(ParseContext(value, 5), {}))); + } +} + +} // namespace +} // namespace Parser +} // namespace CdnLoop +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/cdn_loop/utils_test.cc b/test/extensions/filters/http/cdn_loop/utils_test.cc new file mode 100644 index 0000000000000..95a4ab1415a36 --- /dev/null +++ b/test/extensions/filters/http/cdn_loop/utils_test.cc @@ -0,0 +1,141 @@ +#include "extensions/filters/http/cdn_loop/utils.h" + +#include "test/test_common/status_utility.h" + +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace CdnLoop { +namespace { + +using ::Envoy::StatusHelpers::IsOkAndHolds; +using ::Envoy::StatusHelpers::StatusIs; + +TEST(CountCdnLoopOccurrencesTest, EmptyHeader) { + EXPECT_THAT(countCdnLoopOccurrences("", "cdn"), IsOkAndHolds(0)); +} + +TEST(CountCdnLoopOccurrencesTest, NoParameterTests) { + // A pseudonym + EXPECT_THAT(countCdnLoopOccurrences("cdn", "cdn"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn, CDN", "cdn"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn, CDN", "CDN"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn, CDN", "foo"), IsOkAndHolds(0)); + EXPECT_THAT(countCdnLoopOccurrences("cdn, cdn, cdn", "cdn"), IsOkAndHolds(3)); + EXPECT_THAT(countCdnLoopOccurrences("cdn, cdn, cdn", "foo"), IsOkAndHolds(0)); + + // A DNS name + EXPECT_THAT(countCdnLoopOccurrences("cdn.example.com", "cdn.example.com"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn.example.com, CDN", "cdn.example.com"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn.example.com, CDN", "CDN"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn.example.com, CDN", "foo"), IsOkAndHolds(0)); + EXPECT_THAT(countCdnLoopOccurrences("cdn.example.com, cdn.example.com, cdn.example.com", + "cdn.example.com"), + IsOkAndHolds(3)); + EXPECT_THAT(countCdnLoopOccurrences("cdn.example.com, cdn.example.com, cdn.example.com", "foo"), + IsOkAndHolds(0)); + + // IPv4 Addresses + EXPECT_THAT(countCdnLoopOccurrences("192.0.2.1", "192.0.2.1"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("192.0.2.1, CDN", "192.0.2.1"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("192.0.2.1, CDN", "CDN"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("192.0.2.1, CDN", "foo"), IsOkAndHolds(0)); + EXPECT_THAT(countCdnLoopOccurrences("192.0.2.1, 192.0.2.1, 192.0.2.1", "192.0.2.1"), + IsOkAndHolds(3)); + EXPECT_THAT(countCdnLoopOccurrences("192.0.2.1, 192.0.2.1, 192.0.2.1", "foo"), IsOkAndHolds(0)); + + // IpV6 Addresses + EXPECT_THAT(countCdnLoopOccurrences("[2001:DB8::3]", "[2001:DB8::3]"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("[2001:DB8::3], CDN", "[2001:DB8::3]"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("[2001:DB8::3], CDN", "CDN"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("[2001:DB8::3], CDN", "foo"), IsOkAndHolds(0)); + EXPECT_THAT( + countCdnLoopOccurrences("[2001:DB8::3], [2001:DB8::3], [2001:DB8::3]", "[2001:DB8::3]"), + IsOkAndHolds(3)); + EXPECT_THAT(countCdnLoopOccurrences("[2001:DB8::3], [2001:DB8::3], [2001:DB8::3]", "foo"), + IsOkAndHolds(0)); +} + +TEST(CountCdnLoopOccurrencesTest, SimpleParameterTests) { + EXPECT_THAT(countCdnLoopOccurrences("cdn; foo=bar", "cdn"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn; foo=bar, CDN", "cdn"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn; foo=bar; baz=quux, CDN", "cdn"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn, cdn; foo=bar, cdn; baz=quux", "cdn"), IsOkAndHolds(3)); + EXPECT_THAT(countCdnLoopOccurrences("cdn, cdn; foo=bar; baz=quux, cdn", "foo"), IsOkAndHolds(0)); +} + +TEST(CountCdnLoopOccurrencesTest, ExcessWhitespace) { + EXPECT_THAT(countCdnLoopOccurrences(" cdn", "cdn"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn ", "cdn"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences(" cdn ", "cdn"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("\tcdn\t", "cdn"), IsOkAndHolds(1)); +} + +TEST(CountCdnLoopOccurrencesTest, NoWhitespace) { + EXPECT_THAT(countCdnLoopOccurrences("cdn", "cdn"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn,cdn", "cdn"), IsOkAndHolds(2)); + EXPECT_THAT(countCdnLoopOccurrences("cdn;foo=bar;baz=quuz,cdn", "cdn"), IsOkAndHolds(2)); +} + +TEST(CountCdnLoopOccurrencesTest, CdnIdInParameterTests) { + // In these tests, the parameter contains a string matching the cdn_id in + // either the key or the value of the parameters. + EXPECT_THAT(countCdnLoopOccurrences("cdn; cdn=bar", "cdn"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn; foo=cdn", "cdn"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn; cdn=cdn", "cdn"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn; cdn=\"cdn\"", "cdn"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn; cdn=\"cdn,cdn\"", "cdn"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn; cdn=\"cdn, cdn\"", "cdn"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences("cdn, cdn; cdn=\"cdn\", cdn ; cdn=\"cdn,cdn\"", "cdn"), + IsOkAndHolds(3)); +} + +TEST(CountCdnLoopOccurrencesTest, Rfc8586Tests) { + // Examples from RFC 8586, Section 2. + const std::string example1 = "foo123.foocdn.example, barcdn.example; trace=\"abcdef\""; + EXPECT_THAT(countCdnLoopOccurrences(example1, "foo123.foocdn.example"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences(example1, "barcdn.example"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences(example1, "trace=\"abcdef\""), IsOkAndHolds(0)); + const std::string example2 = "AnotherCDN; abc=123; def=\"456\""; + EXPECT_THAT(countCdnLoopOccurrences(example2, "AnotherCDN"), IsOkAndHolds(1)); + + // The concatenation of the two done correctly as per RFC 7230 rules + { + const std::string combined = absl::StrCat(example1, ",", example2); + EXPECT_THAT(countCdnLoopOccurrences(combined, "foo123.foocdn.example"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences(combined, "barcdn.example"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences(combined, "AnotherCDN"), IsOkAndHolds(1)); + } + + // The concatenation of two done poorly (with extra commas) + { + const std::string combined = absl::StrCat(example1, ",,,", example2); + EXPECT_THAT(countCdnLoopOccurrences(combined, "foo123.foocdn.example"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences(combined, "barcdn.example"), IsOkAndHolds(1)); + EXPECT_THAT(countCdnLoopOccurrences(combined, "AnotherCDN"), IsOkAndHolds(1)); + } +} + +TEST(CountCdnLoopOccurrencesTest, ValidHeaderInsideParameter) { + EXPECT_THAT(countCdnLoopOccurrences("cdn; header=\"cdn; cdn=cdn; cdn\"", "cdn"), IsOkAndHolds(1)); +} + +TEST(CountCdnLoopOccurrencesTest, BadCdnId) { + EXPECT_THAT(countCdnLoopOccurrences("cdn", ""), StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(CountCdnLoopOccurrencesTest, BadHeader) { + EXPECT_THAT(countCdnLoopOccurrences("[bad-id", "cdn"), + StatusIs(absl::StatusCode::kInvalidArgument)); +} + +} // namespace +} // namespace CdnLoop +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/test_common/BUILD b/test/test_common/BUILD index 97be022e4d02e..90cd94c91af92 100644 --- a/test/test_common/BUILD +++ b/test/test_common/BUILD @@ -274,6 +274,11 @@ envoy_cc_test_library( ], ) +envoy_cc_test_library( + name = "status_utility_lib", + hdrs = ["status_utility.h"], +) + envoy_cc_test( name = "simulated_time_system_test", srcs = ["simulated_time_system_test.cc"], diff --git a/test/test_common/status_utility.h b/test/test_common/status_utility.h new file mode 100644 index 0000000000000..5a48a7c2e0f33 --- /dev/null +++ b/test/test_common/status_utility.h @@ -0,0 +1,42 @@ +#pragma once + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace StatusHelpers { + +// Check that a StatusOr is OK and has a value equal to its argument. +// +// For example: +// +// StatusOr status(3); +// EXPECT_THAT(status, IsOkAndHolds(3)); +MATCHER_P(IsOkAndHolds, expected, "") { + if (!arg) { + *result_listener << "which has unexpected status: " << arg.status(); + return false; + } + if (*arg != expected) { + *result_listener << "which has wrong value: " << *arg; + return false; + } + return true; +} + +// Check that a StatusOr as a status code equal to its argument. +// +// For example: +// +// StatusOr status(absl::InvalidArgumentError("bad argument!")); +// EXPECT_THAT(status, StatusIs(absl::StatusCode::kInvalidArgument)); +MATCHER_P(StatusIs, expected_code, "") { + if (arg.status().code() != expected_code) { + *result_listener << "which has unexpected status: " << arg.status(); + return false; + } + return true; +} + +} // namespace StatusHelpers +} // namespace Envoy diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index d2bf372e07cb9..3f759dc47e48b 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -24,10 +24,14 @@ BSON BPF CAS CB +CDN CDS CEL DSR +HEXDIG +HEXDIGIT LTT +OWS TIDs ceil CHACHA