Skip to content

Commit ceddde2

Browse files
authored
router: support downstream cert formatting in the header formatter (envoyproxy#14830)
This extends downstream cert formatting capabilities to the router's header formatter. Risk Level: Low Testing: Unit tests Docs Changes: Updated custom request/response header docs Release Notes: added Fixes envoyproxy#14501
1 parent dd77013 commit ceddde2

File tree

5 files changed

+81
-44
lines changed

5 files changed

+81
-44
lines changed

docs/root/configuration/http/http_conn_man/headers.rst

+6
Original file line numberDiff line numberDiff line change
@@ -605,12 +605,18 @@ Supported variable names are:
605605
TCP
606606
The validity start date of the client certificate used to establish the downstream TLS connection.
607607

608+
DOWNSTREAM_PEER_CERT_V_START can be customized with specifiers as specified in
609+
:ref:`access log format rules<config_access_log_format_downstream_peer_cert_v_start>`.
610+
608611
%DOWNSTREAM_PEER_CERT_V_END%
609612
HTTP
610613
The validity end date of the client certificate used to establish the downstream TLS connection.
611614
TCP
612615
The validity end date of the client certificate used to establish the downstream TLS connection.
613616

617+
DOWNSTREAM_PEER_CERT_V_END can be customized with specifiers as specified in
618+
:ref:`access log format rules<config_access_log_format_downstream_peer_cert_v_end>`.
619+
614620
%HOSTNAME%
615621
The system hostname.
616622

docs/root/version_history/current.rst

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Minor Behavior Changes
3535
`envoy.reloadable_features.remove_forked_chromium_url`.
3636
* oauth filter: added the optional parameter :ref:`auth_scopes <envoy_v3_api_field_extensions.filters.http.oauth2.v3alpha.OAuth2Config.auth_scopes>` with default value of 'user' if not provided. Enables this value to be overridden in the Authorization request to the OAuth provider.
3737
* perf: allow reading more bytes per operation from raw sockets to improve performance.
38+
* router: extended custom date formatting to DOWNSTREAM_PEER_CERT_V_START and DOWNSTREAM_PEER_CERT_V_END when using :ref:`custom request/response header formats <config_http_conn_man_headers_custom_request_headers>`.
3839
* tcp: setting NODELAY in the base connection class. This should have no effect for TCP or HTTP proxying, but may improve throughput in other areas. This behavior can be temporarily reverted by setting `envoy.reloadable_features.always_nodelay` to false.
3940
* upstream: host weight changes now cause a full load balancer rebuild as opposed to happening
4041
atomically inline. This change has been made to support load balancer pre-computation of data

source/common/router/header_formatter.cc

+24-42
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,25 @@ std::string formatPerRequestStateParseException(absl::string_view params) {
4343
params);
4444
}
4545

46+
// Parses a substitution format field and returns a function that formats it.
47+
std::function<std::string(const Envoy::StreamInfo::StreamInfo&)>
48+
parseSubstitutionFormatField(absl::string_view field_name,
49+
StreamInfoHeaderFormatter::FormatterPtrMap& formatter_map) {
50+
const std::string pattern = fmt::format("%{}%", field_name);
51+
if (formatter_map.find(pattern) == formatter_map.end()) {
52+
formatter_map.emplace(
53+
std::make_pair(pattern, Formatter::FormatterPtr(new Formatter::FormatterImpl(
54+
pattern, /*omit_empty_values=*/true))));
55+
}
56+
return [&formatter_map, pattern](const Envoy::StreamInfo::StreamInfo& stream_info) {
57+
const auto& formatter = formatter_map.at(pattern);
58+
return formatter->format(*Http::StaticEmptyHeaders::get().request_headers,
59+
*Http::StaticEmptyHeaders::get().response_headers,
60+
*Http::StaticEmptyHeaders::get().response_trailers, stream_info,
61+
absl::string_view());
62+
};
63+
}
64+
4665
// Parses the parameters for UPSTREAM_METADATA and returns a function suitable for accessing the
4766
// specified metadata from an StreamInfo::StreamInfo. Expects a string formatted as:
4867
// (["a", "b", "c"])
@@ -210,21 +229,6 @@ StreamInfoHeaderFormatter::FieldExtractor sslConnectionInfoStringHeaderExtractor
210229
};
211230
}
212231

213-
// Helper that handles the case when the desired time field is empty.
214-
StreamInfoHeaderFormatter::FieldExtractor sslConnectionInfoStringTimeHeaderExtractor(
215-
std::function<absl::optional<SystemTime>(const Ssl::ConnectionInfo& connection_info)>
216-
time_extractor) {
217-
return sslConnectionInfoStringHeaderExtractor(
218-
[time_extractor](const Ssl::ConnectionInfo& connection_info) {
219-
absl::optional<SystemTime> time = time_extractor(connection_info);
220-
if (!time.has_value()) {
221-
return std::string();
222-
}
223-
224-
return AccessLogDateTimeFormatter::fromTime(time.value());
225-
});
226-
}
227-
228232
} // namespace
229233

230234
StreamInfoHeaderFormatter::StreamInfoHeaderFormatter(absl::string_view field_name, bool append)
@@ -313,16 +317,10 @@ StreamInfoHeaderFormatter::StreamInfoHeaderFormatter(absl::string_view field_nam
313317
sslConnectionInfoStringHeaderExtractor([](const Ssl::ConnectionInfo& connection_info) {
314318
return connection_info.urlEncodedPemEncodedPeerCertificate();
315319
});
316-
} else if (field_name == "DOWNSTREAM_PEER_CERT_V_START") {
317-
field_extractor_ =
318-
sslConnectionInfoStringTimeHeaderExtractor([](const Ssl::ConnectionInfo& connection_info) {
319-
return connection_info.validFromPeerCertificate();
320-
});
321-
} else if (field_name == "DOWNSTREAM_PEER_CERT_V_END") {
322-
field_extractor_ =
323-
sslConnectionInfoStringTimeHeaderExtractor([](const Ssl::ConnectionInfo& connection_info) {
324-
return connection_info.expirationPeerCertificate();
325-
});
320+
} else if (absl::StartsWith(field_name, "DOWNSTREAM_PEER_CERT_V_START")) {
321+
field_extractor_ = parseSubstitutionFormatField(field_name, formatter_map_);
322+
} else if (absl::StartsWith(field_name, "DOWNSTREAM_PEER_CERT_V_END")) {
323+
field_extractor_ = parseSubstitutionFormatField(field_name, formatter_map_);
326324
} else if (field_name == "UPSTREAM_REMOTE_ADDRESS") {
327325
field_extractor_ = [](const Envoy::StreamInfo::StreamInfo& stream_info) -> std::string {
328326
if (stream_info.upstreamHost()) {
@@ -331,23 +329,7 @@ StreamInfoHeaderFormatter::StreamInfoHeaderFormatter(absl::string_view field_nam
331329
return "";
332330
};
333331
} else if (absl::StartsWith(field_name, "START_TIME")) {
334-
const std::string pattern = fmt::format("%{}%", field_name);
335-
if (start_time_formatters_.find(pattern) == start_time_formatters_.end()) {
336-
start_time_formatters_.emplace(
337-
std::make_pair(pattern, Formatter::SubstitutionFormatParser::parse(pattern)));
338-
}
339-
field_extractor_ = [this, pattern](const Envoy::StreamInfo::StreamInfo& stream_info) {
340-
const auto& formatters = start_time_formatters_.at(pattern);
341-
std::string formatted;
342-
for (const auto& formatter : formatters) {
343-
const auto bit = formatter->format(*Http::StaticEmptyHeaders::get().request_headers,
344-
*Http::StaticEmptyHeaders::get().response_headers,
345-
*Http::StaticEmptyHeaders::get().response_trailers,
346-
stream_info, absl::string_view());
347-
absl::StrAppend(&formatted, bit.value_or("-"));
348-
}
349-
return formatted;
350-
};
332+
field_extractor_ = parseSubstitutionFormatField(field_name, formatter_map_);
351333
} else if (absl::StartsWith(field_name, "UPSTREAM_METADATA")) {
352334
field_extractor_ = parseMetadataField(field_name.substr(STATIC_STRLEN("UPSTREAM_METADATA")));
353335
} else if (absl::StartsWith(field_name, "DYNAMIC_METADATA")) {

source/common/router/header_formatter.h

+8-2
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,18 @@ class StreamInfoHeaderFormatter : public HeaderFormatter {
4242
bool append() const override { return append_; }
4343

4444
using FieldExtractor = std::function<std::string(const Envoy::StreamInfo::StreamInfo&)>;
45+
using FormatterPtrMap = absl::node_hash_map<std::string, Envoy::Formatter::FormatterPtr>;
4546

4647
private:
4748
FieldExtractor field_extractor_;
4849
const bool append_;
49-
absl::node_hash_map<std::string, std::vector<Envoy::Formatter::FormatterProviderPtr>>
50-
start_time_formatters_;
50+
51+
// Maps a string format pattern (including field name and any command operators between
52+
// parenthesis) to the list of FormatterProviderPtrs that are capable of formatting that pattern.
53+
// We use a map here to make sure that we only create a single parser for a given format pattern
54+
// even if it appears multiple times in the larger formatting context (e.g. it shows up multiple
55+
// times in a format string).
56+
FormatterPtrMap formatter_map_;
5157
};
5258

5359
/**

test/common/router/header_formatter_test.cc

+42
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,18 @@ TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVStart) {
463463
testFormatting(stream_info, "DOWNSTREAM_PEER_CERT_V_START", "2018-12-18T01:50:34.000Z");
464464
}
465465

466+
TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVStartCustom) {
467+
NiceMock<Envoy::StreamInfo::MockStreamInfo> stream_info;
468+
auto connection_info = std::make_shared<NiceMock<Ssl::MockConnectionInfo>>();
469+
absl::Time abslStartTime =
470+
TestUtility::parseTime("Dec 18 01:50:34 2018 GMT", "%b %e %H:%M:%S %Y GMT");
471+
SystemTime startTime = absl::ToChronoTime(abslStartTime);
472+
ON_CALL(*connection_info, validFromPeerCertificate()).WillByDefault(Return(startTime));
473+
EXPECT_CALL(stream_info, downstreamSslConnection()).WillRepeatedly(Return(connection_info));
474+
testFormatting(stream_info, "DOWNSTREAM_PEER_CERT_V_START(%b %e %H:%M:%S %Y %Z)",
475+
"Dec 18 01:50:34 2018 UTC");
476+
}
477+
466478
TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVStartEmpty) {
467479
NiceMock<Envoy::StreamInfo::MockStreamInfo> stream_info;
468480
auto connection_info = std::make_shared<NiceMock<Ssl::MockConnectionInfo>>();
@@ -488,6 +500,18 @@ TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVEnd) {
488500
testFormatting(stream_info, "DOWNSTREAM_PEER_CERT_V_END", "2020-12-17T01:50:34.000Z");
489501
}
490502

503+
TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVEndCustom) {
504+
NiceMock<Envoy::StreamInfo::MockStreamInfo> stream_info;
505+
auto connection_info = std::make_shared<NiceMock<Ssl::MockConnectionInfo>>();
506+
absl::Time abslStartTime =
507+
TestUtility::parseTime("Dec 17 01:50:34 2020 GMT", "%b %e %H:%M:%S %Y GMT");
508+
SystemTime startTime = absl::ToChronoTime(abslStartTime);
509+
ON_CALL(*connection_info, expirationPeerCertificate()).WillByDefault(Return(startTime));
510+
EXPECT_CALL(stream_info, downstreamSslConnection()).WillRepeatedly(Return(connection_info));
511+
testFormatting(stream_info, "DOWNSTREAM_PEER_CERT_V_END(%b %e %H:%M:%S %Y %Z)",
512+
"Dec 17 01:50:34 2020 UTC");
513+
}
514+
491515
TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVEndEmpty) {
492516
NiceMock<Envoy::StreamInfo::MockStreamInfo> stream_info;
493517
auto connection_info = std::make_shared<NiceMock<Ssl::MockConnectionInfo>>();
@@ -502,6 +526,24 @@ TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVEndNoTls)
502526
testFormatting(stream_info, "DOWNSTREAM_PEER_CERT_V_END", EMPTY_STRING);
503527
}
504528

529+
TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithStartTime) {
530+
NiceMock<Envoy::StreamInfo::MockStreamInfo> stream_info;
531+
absl::Time abslStartTime =
532+
TestUtility::parseTime("Dec 17 01:50:34 2020 GMT", "%b %e %H:%M:%S %Y GMT");
533+
SystemTime startTime = absl::ToChronoTime(abslStartTime);
534+
EXPECT_CALL(stream_info, startTime()).WillRepeatedly(Return(startTime));
535+
testFormatting(stream_info, "START_TIME", "2020-12-17T01:50:34.000Z");
536+
}
537+
538+
TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithStartTimeCustom) {
539+
NiceMock<Envoy::StreamInfo::MockStreamInfo> stream_info;
540+
absl::Time abslStartTime =
541+
TestUtility::parseTime("Dec 17 01:50:34 2020 GMT", "%b %e %H:%M:%S %Y GMT");
542+
SystemTime startTime = absl::ToChronoTime(abslStartTime);
543+
EXPECT_CALL(stream_info, startTime()).WillRepeatedly(Return(startTime));
544+
testFormatting(stream_info, "START_TIME(%b %e %H:%M:%S %Y %Z)", "Dec 17 01:50:34 2020 UTC");
545+
}
546+
505547
TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithUpstreamMetadataVariable) {
506548
NiceMock<Envoy::StreamInfo::MockStreamInfo> stream_info;
507549
std::shared_ptr<NiceMock<Envoy::Upstream::MockHostDescription>> host(

0 commit comments

Comments
 (0)