1
#include "source/extensions/formatter/xfcc_value/xfcc_value.h"
2

            
3
#include <cstddef>
4
#include <ranges>
5

            
6
namespace Envoy {
7
namespace Extensions {
8
namespace Formatter {
9

            
10
namespace {
11

            
12
2
const absl::flat_hash_set<std::string>& supportedKeys() {
13
  // The keys are case-insensitive, so we store them in lower case.
14
2
  CONSTRUCT_ON_FIRST_USE(absl::flat_hash_set<std::string>, {
15
2
                                                               "by",
16
2
                                                               "hash",
17
2
                                                               "cert",
18
2
                                                               "chain",
19
2
                                                               "subject",
20
2
                                                               "uri",
21
2
                                                               "dns",
22
2
                                                           });
23
2
}
24

            
25
23
size_t countBackslashes(absl::string_view str) {
26
23
  size_t count = 0;
27
  // Search from end to start for first not '\'
28
37
  for (char r : std::ranges::reverse_view(str)) {
29
37
    if (r == '\\') {
30
14
      ++count;
31
23
    } else {
32
23
      break;
33
23
    }
34
37
  }
35
23
  return count;
36
23
}
37

            
38
// The XFCC header value is a comma (`,`) separated string. Each substring is an XFCC element,
39
// which holds information added by a single proxy. A proxy can append the current client
40
// certificate information as an XFCC element, to the end of the request’s XFCC header after a
41
// comma.
42
//
43
// Each XFCC element is a semicolon (`;`) separated string. Each substring is a key-value pair,
44
// grouped together by an equals (`=`) sign. The keys are case-insensitive, the values are
45
// case-sensitive. If  `,`, `;` or `=` appear in a value, the value should be double-quoted.
46
// Double-quotes in the value should be replaced by backslash-double-quote (`\"`).
47
//
48
// There maybe multiple XFCC elements and the oldest/leftest one with the key will be used by
49
// default because Envoy assumes the oldest XFCC element come from the original client certificate.
50
// So scan the header left-to-right.
51

            
52
// Handles a single key/value pair within an XFCC element.
53
18
absl::optional<std::string> parseKeyValuePair(absl::string_view pair, absl::string_view target) {
54
  // Find '=' not in quotes. Because key will always be the first part and won't be double
55
  // quoted or contain `=`, we can safely use `absl::StrSplit`.
56
18
  std::pair<absl::string_view, absl::string_view> key_value =
57
18
      absl::StrSplit(pair, absl::MaxSplits('=', 1));
58
18
  absl::string_view raw_key = absl::StripAsciiWhitespace(key_value.first);
59
18
  if (!absl::EqualsIgnoreCase(raw_key, target)) {
60
13
    return absl::nullopt;
61
13
  }
62

            
63
5
  absl::string_view raw_value = absl::StripAsciiWhitespace(key_value.second);
64
  // If value is double quoted, remove quotes.
65
5
  if (raw_value.size() >= 2 && raw_value.front() == '"' && raw_value.back() == '"') {
66
4
    raw_value = raw_value.substr(1, raw_value.size() - 2);
67
4
  }
68

            
69
  // Quick path to avoid handle unescaping if not needed.
70
5
  if (raw_value.find('\\') == absl::string_view::npos) {
71
1
    return std::string(raw_value);
72
1
  }
73

            
74
  // Handle unescaping.
75

            
76
  // If the raw value only contains a single backslash then return it as is.
77
4
  if (raw_value.size() < 2) {
78
    return std::string(raw_value);
79
  }
80

            
81
  // Unescape double quotes and backslashes.
82
4
  std::string unescaped;
83
4
  unescaped.reserve(raw_value.size());
84
4
  size_t i = 0;
85
33
  for (; i < raw_value.size() - 1; ++i) {
86
29
    if (raw_value[i] == '\\') {
87
5
      if (raw_value[i + 1] == '"' || raw_value[i + 1] == '\\') {
88
5
        unescaped.push_back(raw_value[i + 1]);
89
5
        ++i;
90
5
        continue;
91
5
      }
92
5
    }
93
24
    unescaped.push_back(raw_value[i]);
94
24
  }
95
  // Handle the last character.
96
4
  if (i < raw_value.size()) {
97
2
    unescaped.push_back(raw_value[i]);
98
2
  }
99

            
100
4
  return unescaped;
101
4
}
102

            
103
// Handles a single XFCC element (semicolon-separated key/value pairs).
104
absl::optional<std::string> parseElementForKey(absl::string_view element,
105
9
                                               absl::string_view target) {
106

            
107
  // Scan key-value pairs in this element (by semicolon not in quotes).
108
9
  bool in_quotes = false;
109
9
  size_t start = 0;
110
9
  const size_t element_size = element.size();
111
212
  for (size_t i = 0; i <= element_size; ++i) {
112
    // Check for end of key-value pair.
113
208
    if (i == element_size || element[i] == ';') {
114
      // If not in quotes then we found the end of a key-value pair.
115
24
      if (!in_quotes) {
116
18
        auto value = parseKeyValuePair(element.substr(start, i - start), target);
117
18
        if (value.has_value()) {
118
5
          return value;
119
5
        }
120
13
        start = i + 1;
121
13
      }
122
19
      continue;
123
24
    }
124

            
125
    // Switch quote state if we encounter a quote character.
126
184
    if (element[i] == '"') {
127
11
      if (countBackslashes(element.substr(0, i)) % 2 == 0) {
128
8
        in_quotes = !in_quotes;
129
8
      }
130
11
    }
131
184
  }
132

            
133
  // Note, we should never encounter unmatched quotes here because if there is
134
  // an unmatched quote, it should be handled in the parseValueFromXfccByKey()
135
  // and will not enter this function.
136
4
  ASSERT(!in_quotes);
137
4
  return absl::nullopt;
138
9
}
139

            
140
// Extracts the key from the XFCC header.
141
absl::StatusOr<std::string> parseValueFromXfccByKey(const Http::RequestHeaderMap& headers,
142
8
                                                    absl::string_view target) {
143
8
  absl::string_view value = headers.getForwardedClientCertValue();
144
8
  if (value.empty()) {
145
1
    return absl::InvalidArgumentError("XFCC header is not present");
146
1
  }
147

            
148
  // Scan elements in the XFCC header (by comma not in quotes).
149
7
  bool in_quotes = false;
150
7
  size_t start = 0;
151
7
  const size_t value_size = value.size();
152
323
  for (size_t i = 0; i <= value_size; ++i) {
153
    // Check for end of element.
154
321
    if (i == value_size || value[i] == ',') {
155
      // If not in quotes then we found the end of an element.
156
14
      if (!in_quotes) {
157
9
        auto result = parseElementForKey(value.substr(start, i - start), target);
158
9
        if (result.has_value()) {
159
5
          return result.value();
160
5
        }
161
4
        start = i + 1;
162
4
      }
163
9
      continue;
164
14
    }
165

            
166
    // Switch quote state if we encounter a quote character.
167
307
    if (value[i] == '"') {
168
12
      if (countBackslashes(value.substr(0, i)) % 2 == 0) {
169
9
        in_quotes = !in_quotes;
170
9
      }
171
12
    }
172
307
  }
173

            
174
2
  if (in_quotes) {
175
1
    return absl::InvalidArgumentError("Invalid XFCC header: unmatched quotes");
176
1
  }
177

            
178
1
  return absl::InvalidArgumentError("XFCC header does not contain target key");
179
2
}
180

            
181
} // namespace
182

            
183
class XfccValueFormatterProvider : public ::Envoy::Formatter::FormatterProvider,
184
                                   Logger::Loggable<Logger::Id::http> {
185
public:
186
1
  XfccValueFormatterProvider(Http::LowerCaseString&& key) : key_(key) {}
187

            
188
  absl::optional<std::string> format(const Envoy::Formatter::Context& context,
189
9
                                     const StreamInfo::StreamInfo&) const override {
190
9
    const auto headers = context.requestHeaders();
191
9
    if (!headers.has_value()) {
192
1
      return absl::nullopt;
193
1
    }
194

            
195
8
    auto status_or = parseValueFromXfccByKey(*headers, key_);
196
8
    if (!status_or.ok()) {
197
3
      ENVOY_LOG(debug, "XFCC value extraction failure: {}", status_or.status().message());
198
3
      return absl::nullopt;
199
3
    }
200
5
    return std::move(status_or.value());
201
8
  }
202

            
203
  Protobuf::Value formatValue(const Envoy::Formatter::Context& context,
204
9
                              const StreamInfo::StreamInfo& stream_info) const override {
205
9
    absl::optional<std::string> value = format(context, stream_info);
206
9
    if (!value.has_value()) {
207
4
      return ValueUtil::nullValue();
208
4
    }
209
5
    Protobuf::Value result;
210
5
    result.set_string_value(std::move(value.value()));
211
5
    return result;
212
9
  }
213

            
214
private:
215
  Http::LowerCaseString key_;
216
};
217

            
218
Envoy::Formatter::FormatterProviderPtr
219
XfccValueFormatterCommandParser::parse(absl::string_view command, absl::string_view subcommand,
220
256
                                       absl::optional<size_t>) const {
221
  // Implementation for parsing the XFCC_VALUE() command.
222
256
  if (command != "XFCC_VALUE") {
223
253
    return nullptr;
224
253
  }
225

            
226
3
  Http::LowerCaseString lower_subcommand(subcommand);
227
3
  if (subcommand.empty()) {
228
1
    throw EnvoyException("XFCC_VALUE command requires a subcommand");
229
1
  }
230
2
  if (!supportedKeys().contains(lower_subcommand.get())) {
231
1
    throw EnvoyException(
232
1
        absl::StrCat("XFCC_VALUE command does not support subcommand: ", lower_subcommand.get()));
233
1
  }
234
1
  return std::make_unique<XfccValueFormatterProvider>(std::move(lower_subcommand));
235
2
}
236

            
237
class XfccValueCommandParserFactory : public Envoy::Formatter::BuiltInCommandParserFactory {
238
public:
239
4
  XfccValueCommandParserFactory() = default;
240
2
  Envoy::Formatter::CommandParserPtr createCommandParser() const override {
241
2
    return std::make_unique<XfccValueFormatterCommandParser>();
242
2
  }
243
4
  std::string name() const override { return "envoy.built_in_formatters.xfcc_value"; }
244
};
245

            
246
REGISTER_FACTORY(XfccValueCommandParserFactory, Envoy::Formatter::BuiltInCommandParserFactory);
247

            
248
} // namespace Formatter
249
} // namespace Extensions
250
} // namespace Envoy