Skip to content

Commit

Permalink
router: support downstream cert formatting in the header formatter (e…
Browse files Browse the repository at this point in the history
…nvoyproxy#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
  • Loading branch information
esmet committed Jun 10, 2021
1 parent 3d0a086 commit 730aadf
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 44 deletions.
6 changes: 6 additions & 0 deletions docs/root/configuration/http/http_conn_man/headers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -605,12 +605,18 @@ Supported variable names are:
TCP
The validity start date of the client certificate used to establish the downstream TLS connection.

DOWNSTREAM_PEER_CERT_V_START can be customized with specifiers as specified in
:ref:`access log format rules<config_access_log_format_downstream_peer_cert_v_start>`.

%DOWNSTREAM_PEER_CERT_V_END%
HTTP
The validity end date of the client certificate used to establish the downstream TLS connection.
TCP
The validity end date of the client certificate used to establish the downstream TLS connection.

DOWNSTREAM_PEER_CERT_V_END can be customized with specifiers as specified in
:ref:`access log format rules<config_access_log_format_downstream_peer_cert_v_end>`.

%HOSTNAME%
The system hostname.

Expand Down
2 changes: 2 additions & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Minor Behavior Changes
----------------------
*Changes that may cause incompatibilities for some users, but should not for most*

* 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>`.

Bug Fixes
---------
*Changes expected to improve the state of the world and are unlikely to have negative effects*
Expand Down
66 changes: 24 additions & 42 deletions source/common/router/header_formatter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,25 @@ std::string formatPerRequestStateParseException(absl::string_view params) {
params);
}

// Parses a substitution format field and returns a function that formats it.
std::function<std::string(const Envoy::StreamInfo::StreamInfo&)>
parseSubstitutionFormatField(absl::string_view field_name,
StreamInfoHeaderFormatter::FormatterPtrMap& formatter_map) {
const std::string pattern = fmt::format("%{}%", field_name);
if (formatter_map.find(pattern) == formatter_map.end()) {
formatter_map.emplace(
std::make_pair(pattern, Formatter::FormatterPtr(new Formatter::FormatterImpl(
pattern, /*omit_empty_values=*/true))));
}
return [&formatter_map, pattern](const Envoy::StreamInfo::StreamInfo& stream_info) {
const auto& formatter = formatter_map.at(pattern);
return formatter->format(*Http::StaticEmptyHeaders::get().request_headers,
*Http::StaticEmptyHeaders::get().response_headers,
*Http::StaticEmptyHeaders::get().response_trailers, stream_info,
absl::string_view());
};
}

// Parses the parameters for UPSTREAM_METADATA and returns a function suitable for accessing the
// specified metadata from an StreamInfo::StreamInfo. Expects a string formatted as:
// (["a", "b", "c"])
Expand Down Expand Up @@ -210,21 +229,6 @@ StreamInfoHeaderFormatter::FieldExtractor sslConnectionInfoStringHeaderExtractor
};
}

// Helper that handles the case when the desired time field is empty.
StreamInfoHeaderFormatter::FieldExtractor sslConnectionInfoStringTimeHeaderExtractor(
std::function<absl::optional<SystemTime>(const Ssl::ConnectionInfo& connection_info)>
time_extractor) {
return sslConnectionInfoStringHeaderExtractor(
[time_extractor](const Ssl::ConnectionInfo& connection_info) {
absl::optional<SystemTime> time = time_extractor(connection_info);
if (!time.has_value()) {
return std::string();
}

return AccessLogDateTimeFormatter::fromTime(time.value());
});
}

} // namespace

StreamInfoHeaderFormatter::StreamInfoHeaderFormatter(absl::string_view field_name, bool append)
Expand Down Expand Up @@ -313,16 +317,10 @@ StreamInfoHeaderFormatter::StreamInfoHeaderFormatter(absl::string_view field_nam
sslConnectionInfoStringHeaderExtractor([](const Ssl::ConnectionInfo& connection_info) {
return connection_info.urlEncodedPemEncodedPeerCertificate();
});
} else if (field_name == "DOWNSTREAM_PEER_CERT_V_START") {
field_extractor_ =
sslConnectionInfoStringTimeHeaderExtractor([](const Ssl::ConnectionInfo& connection_info) {
return connection_info.validFromPeerCertificate();
});
} else if (field_name == "DOWNSTREAM_PEER_CERT_V_END") {
field_extractor_ =
sslConnectionInfoStringTimeHeaderExtractor([](const Ssl::ConnectionInfo& connection_info) {
return connection_info.expirationPeerCertificate();
});
} else if (absl::StartsWith(field_name, "DOWNSTREAM_PEER_CERT_V_START")) {
field_extractor_ = parseSubstitutionFormatField(field_name, formatter_map_);
} else if (absl::StartsWith(field_name, "DOWNSTREAM_PEER_CERT_V_END")) {
field_extractor_ = parseSubstitutionFormatField(field_name, formatter_map_);
} else if (field_name == "UPSTREAM_REMOTE_ADDRESS") {
field_extractor_ = [](const Envoy::StreamInfo::StreamInfo& stream_info) -> std::string {
if (stream_info.upstreamHost()) {
Expand All @@ -331,23 +329,7 @@ StreamInfoHeaderFormatter::StreamInfoHeaderFormatter(absl::string_view field_nam
return "";
};
} else if (absl::StartsWith(field_name, "START_TIME")) {
const std::string pattern = fmt::format("%{}%", field_name);
if (start_time_formatters_.find(pattern) == start_time_formatters_.end()) {
start_time_formatters_.emplace(
std::make_pair(pattern, Formatter::SubstitutionFormatParser::parse(pattern)));
}
field_extractor_ = [this, pattern](const Envoy::StreamInfo::StreamInfo& stream_info) {
const auto& formatters = start_time_formatters_.at(pattern);
std::string formatted;
for (const auto& formatter : formatters) {
const auto bit = formatter->format(*Http::StaticEmptyHeaders::get().request_headers,
*Http::StaticEmptyHeaders::get().response_headers,
*Http::StaticEmptyHeaders::get().response_trailers,
stream_info, absl::string_view());
absl::StrAppend(&formatted, bit.value_or("-"));
}
return formatted;
};
field_extractor_ = parseSubstitutionFormatField(field_name, formatter_map_);
} else if (absl::StartsWith(field_name, "UPSTREAM_METADATA")) {
field_extractor_ = parseMetadataField(field_name.substr(STATIC_STRLEN("UPSTREAM_METADATA")));
} else if (absl::StartsWith(field_name, "DYNAMIC_METADATA")) {
Expand Down
10 changes: 8 additions & 2 deletions source/common/router/header_formatter.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,18 @@ class StreamInfoHeaderFormatter : public HeaderFormatter {
bool append() const override { return append_; }

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

private:
FieldExtractor field_extractor_;
const bool append_;
absl::node_hash_map<std::string, std::vector<Envoy::Formatter::FormatterProviderPtr>>
start_time_formatters_;

// Maps a string format pattern (including field name and any command operators between
// parenthesis) to the list of FormatterProviderPtrs that are capable of formatting that pattern.
// We use a map here to make sure that we only create a single parser for a given format pattern
// even if it appears multiple times in the larger formatting context (e.g. it shows up multiple
// times in a format string).
FormatterPtrMap formatter_map_;
};

/**
Expand Down
42 changes: 42 additions & 0 deletions test/common/router/header_formatter_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,18 @@ TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVStart) {
testFormatting(stream_info, "DOWNSTREAM_PEER_CERT_V_START", "2018-12-18T01:50:34.000Z");
}

TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVStartCustom) {
NiceMock<Envoy::StreamInfo::MockStreamInfo> stream_info;
auto connection_info = std::make_shared<NiceMock<Ssl::MockConnectionInfo>>();
absl::Time abslStartTime =
TestUtility::parseTime("Dec 18 01:50:34 2018 GMT", "%b %e %H:%M:%S %Y GMT");
SystemTime startTime = absl::ToChronoTime(abslStartTime);
ON_CALL(*connection_info, validFromPeerCertificate()).WillByDefault(Return(startTime));
EXPECT_CALL(stream_info, downstreamSslConnection()).WillRepeatedly(Return(connection_info));
testFormatting(stream_info, "DOWNSTREAM_PEER_CERT_V_START(%b %e %H:%M:%S %Y %Z)",
"Dec 18 01:50:34 2018 UTC");
}

TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVStartEmpty) {
NiceMock<Envoy::StreamInfo::MockStreamInfo> stream_info;
auto connection_info = std::make_shared<NiceMock<Ssl::MockConnectionInfo>>();
Expand All @@ -488,6 +500,18 @@ TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVEnd) {
testFormatting(stream_info, "DOWNSTREAM_PEER_CERT_V_END", "2020-12-17T01:50:34.000Z");
}

TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVEndCustom) {
NiceMock<Envoy::StreamInfo::MockStreamInfo> stream_info;
auto connection_info = std::make_shared<NiceMock<Ssl::MockConnectionInfo>>();
absl::Time abslStartTime =
TestUtility::parseTime("Dec 17 01:50:34 2020 GMT", "%b %e %H:%M:%S %Y GMT");
SystemTime startTime = absl::ToChronoTime(abslStartTime);
ON_CALL(*connection_info, expirationPeerCertificate()).WillByDefault(Return(startTime));
EXPECT_CALL(stream_info, downstreamSslConnection()).WillRepeatedly(Return(connection_info));
testFormatting(stream_info, "DOWNSTREAM_PEER_CERT_V_END(%b %e %H:%M:%S %Y %Z)",
"Dec 17 01:50:34 2020 UTC");
}

TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVEndEmpty) {
NiceMock<Envoy::StreamInfo::MockStreamInfo> stream_info;
auto connection_info = std::make_shared<NiceMock<Ssl::MockConnectionInfo>>();
Expand All @@ -502,6 +526,24 @@ TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVEndNoTls)
testFormatting(stream_info, "DOWNSTREAM_PEER_CERT_V_END", EMPTY_STRING);
}

TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithStartTime) {
NiceMock<Envoy::StreamInfo::MockStreamInfo> stream_info;
absl::Time abslStartTime =
TestUtility::parseTime("Dec 17 01:50:34 2020 GMT", "%b %e %H:%M:%S %Y GMT");
SystemTime startTime = absl::ToChronoTime(abslStartTime);
EXPECT_CALL(stream_info, startTime()).WillRepeatedly(Return(startTime));
testFormatting(stream_info, "START_TIME", "2020-12-17T01:50:34.000Z");
}

TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithStartTimeCustom) {
NiceMock<Envoy::StreamInfo::MockStreamInfo> stream_info;
absl::Time abslStartTime =
TestUtility::parseTime("Dec 17 01:50:34 2020 GMT", "%b %e %H:%M:%S %Y GMT");
SystemTime startTime = absl::ToChronoTime(abslStartTime);
EXPECT_CALL(stream_info, startTime()).WillRepeatedly(Return(startTime));
testFormatting(stream_info, "START_TIME(%b %e %H:%M:%S %Y %Z)", "Dec 17 01:50:34 2020 UTC");
}

TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithUpstreamMetadataVariable) {
NiceMock<Envoy::StreamInfo::MockStreamInfo> stream_info;
std::shared_ptr<NiceMock<Envoy::Upstream::MockHostDescription>> host(
Expand Down

0 comments on commit 730aadf

Please sign in to comment.