Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

formatter: add custom date formatting to downstream cert start and end dates #14502

Merged
merged 10 commits into from
Jan 6, 2021
6 changes: 6 additions & 0 deletions docs/root/configuration/observability/access_log/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -541,12 +541,18 @@ The following command operators are supported:
TCP
The validity start date of the client certificate used to establish the downstream TLS connection.

DOWNSTREAM_PEER_CERT_V_START can be customized using a `format string <https://en.cppreference.com/w/cpp/io/manip/put_time>`_.
See START_TIME for additional format specifiers and examples.
mattklein123 marked this conversation as resolved.
Show resolved Hide resolved

%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 using a `format string <https://en.cppreference.com/w/cpp/io/manip/put_time>`_.
See START_TIME for additional format specifiers and examples.

%HOSTNAME%
The system hostname.

Expand Down
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ New Features
* config: added ability to flush stats when the admin's :ref:`/stats endpoint <operations_admin_interface_stats>` is hit instead of on a timer via :ref:`stats_flush_on_admin <envoy_v3_api_field_config.bootstrap.v3.Bootstrap.stats_flush_on_admin>`.
* config: added new runtime feature `envoy.features.enable_all_deprecated_features` that allows the use of all deprecated features.
* formatter: added new :ref:`text_format_source <envoy_v3_api_field_config.core.v3.SubstitutionFormatString.text_format_source>` field to support format strings both inline and from a file.
* formatter: added support for custom date formatting to %DOWNSTREAM_PEER_CERT_V_START% and %DOWNSTREAM_PEER_CERT_V_END%, similar to %START_TIME%.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please ref link to the relevant fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

* grpc: implemented header value syntax support when defining :ref:`initial metadata <envoy_v3_api_field_config.core.v3.GrpcService.initial_metadata>` for gRPC-based `ext_authz` :ref:`HTTP <envoy_v3_api_field_extensions.filters.http.ext_authz.v3.ExtAuthz.grpc_service>` and :ref:`network <envoy_v3_api_field_extensions.filters.network.ext_authz.v3.ExtAuthz.grpc_service>` filters, and :ref:`ratelimit <envoy_v3_api_field_config.ratelimit.v3.RateLimitServiceConfig.grpc_service>` filters.
* grpc-json: added support for configuring :ref:`unescaping behavior <envoy_v3_api_field_extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder.url_unescape_spec>` for path components.
* hds: added support for delta updates in the :ref:`HealthCheckSpecifier <envoy_v3_api_msg_service.health.v3.HealthCheckSpecifier>`, making only the Endpoints and Health Checkers that changed be reconstructed on receiving a new message, rather than the entire HDS.
Expand Down
110 changes: 70 additions & 40 deletions source/common/formatter/substitution_formatter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ void truncate(std::string& str, absl::optional<uint32_t> max_length) {
str = str.substr(0, max_length.value());
}

// Matches newline pattern in a StartTimeFormatter format string.
const std::regex& getStartTimeNewlinePattern() {
// Matches newline pattern in a system time format string (e.g. start time)
const std::regex& getSystemTimeFormatNewlinePattern() {
CONSTRUCT_ON_FIRST_USE(std::regex, "%[-_0^#]*[1-9]*(E|O)?n");
}
const std::regex& getNewlinePattern() { CONSTRUCT_ON_FIRST_USE(std::regex, "\n"); }
Expand Down Expand Up @@ -370,18 +370,25 @@ std::vector<FormatterProviderPtr> SubstitutionFormatParser::parse(const std::str
formatters.push_back(
std::make_unique<FilterStateFormatter>(key, max_length, serialize_as_string));
} else if (absl::StartsWith(token, "START_TIME")) {
const size_t parameters_length = pos + StartTimeParamStart + 1;
const size_t parameters_end = command_end_position - parameters_length;

const size_t parameters_length = token.length() - (StartTimeParamStart + 1);
const std::string args = token[StartTimeParamStart - 1] == '('
? token.substr(StartTimeParamStart, parameters_end)
? token.substr(StartTimeParamStart, parameters_length)
: "";
// Validate the input specifier here. The formatted string may be destined for a header, and
// should not contain invalid characters {NUL, LR, CF}.
if (std::regex_search(args, getStartTimeNewlinePattern())) {
throw EnvoyException("Invalid header configuration. Format string contains newline.");
}
formatters.emplace_back(FormatterProviderPtr{new StartTimeFormatter(args)});
} else if (absl::StartsWith(token, "DOWNSTREAM_PEER_CERT_V_START")) {
mattklein123 marked this conversation as resolved.
Show resolved Hide resolved
const size_t parameters_length = token.length() - (DownstreamPeerCertVStartParamStart + 1);
const std::string args =
token[DownstreamPeerCertVStartParamStart - 1] == '('
? token.substr(DownstreamPeerCertVStartParamStart, parameters_length)
: "";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this part be somehow pushed down into the shared parser/formatter? It's pretty confusing and could use more comments anyway. Until this code is rewritten to use an actual parser grammar I would err on the side of more comments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds right. I'll add a shared format parsing function and push the logic into each constructor, which should allow this to become an easy one liner.

formatters.emplace_back(FormatterProviderPtr{new DownstreamPeerCertVStartFormatter(args)});
} else if (absl::StartsWith(token, "DOWNSTREAM_PEER_CERT_V_END")) {
const size_t parameters_length = token.length() - (DownstreamPeerCertVEndParamStart + 1);
const std::string args =
token[DownstreamPeerCertVEndParamStart - 1] == '('
? token.substr(DownstreamPeerCertVEndParamStart, parameters_length)
: "";
formatters.emplace_back(FormatterProviderPtr{new DownstreamPeerCertVEndFormatter(args)});
} else if (absl::StartsWith(token, "GRPC_STATUS")) {
formatters.emplace_back(FormatterProviderPtr{
new GrpcStatusFormatter("grpc-status", "", absl::optional<size_t>())});
Expand Down Expand Up @@ -770,26 +777,6 @@ StreamInfoFormatter::StreamInfoFormatter(const std::string& field_name) {
[](const Ssl::ConnectionInfo& connection_info) {
return connection_info.urlEncodedPemEncodedPeerCertificate();
});
} else if (field_name == "DOWNSTREAM_PEER_CERT_V_START") {
field_extractor_ = std::make_unique<StreamInfoSslConnectionInfoFieldExtractor>(
[](const Ssl::ConnectionInfo& connection_info) {
absl::optional<SystemTime> time = connection_info.validFromPeerCertificate();
absl::optional<std::string> result;
if (time.has_value()) {
result = AccessLogDateTimeFormatter::fromTime(time.value());
}
return result;
});
} else if (field_name == "DOWNSTREAM_PEER_CERT_V_END") {
field_extractor_ = std::make_unique<StreamInfoSslConnectionInfoFieldExtractor>(
[](const Ssl::ConnectionInfo& connection_info) {
absl::optional<SystemTime> time = connection_info.expirationPeerCertificate();
absl::optional<std::string> result;
if (time.has_value()) {
result = AccessLogDateTimeFormatter::fromTime(time.value());
}
return result;
});
} else if (field_name == "UPSTREAM_TRANSPORT_FAILURE_REASON") {
field_extractor_ = std::make_unique<StreamInfoStringFieldExtractor>(
[](const StreamInfo::StreamInfo& stream_info) {
Expand Down Expand Up @@ -1131,20 +1118,63 @@ ProtobufWkt::Value FilterStateFormatter::formatValue(const Http::RequestHeaderMa
return val;
}

StartTimeFormatter::StartTimeFormatter(const std::string& format) : date_formatter_(format) {}
// A SystemTime formatter that extracts the startTime from StreamInfo
StartTimeFormatter::StartTimeFormatter(const std::string& format)
: SystemTimeFormatter(
format, std::make_unique<SystemTimeFormatter::TimeFieldExtractor>(
[](const StreamInfo::StreamInfo& stream_info) -> absl::optional<SystemTime> {
return stream_info.startTime();
})) {}

// A SystemTime formatter that optionally extracts the start date from the downstream peer's
// certificate
DownstreamPeerCertVStartFormatter::DownstreamPeerCertVStartFormatter(const std::string& format)
: SystemTimeFormatter(
format, std::make_unique<SystemTimeFormatter::TimeFieldExtractor>(
[](const StreamInfo::StreamInfo& stream_info) -> absl::optional<SystemTime> {
const auto connection_info = stream_info.downstreamSslConnection();
return connection_info != nullptr
? connection_info->validFromPeerCertificate()
: absl::optional<SystemTime>();
})) {}

// A SystemTime formatter that optionally extracts the end date from the downstream peer's
// certificate
DownstreamPeerCertVEndFormatter::DownstreamPeerCertVEndFormatter(const std::string& format)
: SystemTimeFormatter(
format, std::make_unique<SystemTimeFormatter::TimeFieldExtractor>(
[](const StreamInfo::StreamInfo& stream_info) -> absl::optional<SystemTime> {
const auto connection_info = stream_info.downstreamSslConnection();
return connection_info != nullptr
? connection_info->expirationPeerCertificate()
: absl::optional<SystemTime>();
})) {}

SystemTimeFormatter::SystemTimeFormatter(const std::string& format, TimeFieldExtractorPtr f)
: date_formatter_(format), time_field_extractor_(std::move(f)) {
// Validate the input specifier here. The formatted string may be destined for a header, and
// should not contain invalid characters {NUL, LR, CF}.
if (std::regex_search(format, getSystemTimeFormatNewlinePattern())) {
throw EnvoyException("Invalid header configuration. Format string contains newline.");
}
}

absl::optional<std::string> StartTimeFormatter::format(const Http::RequestHeaderMap&,
const Http::ResponseHeaderMap&,
const Http::ResponseTrailerMap&,
const StreamInfo::StreamInfo& stream_info,
absl::string_view) const {
absl::optional<std::string> SystemTimeFormatter::format(const Http::RequestHeaderMap&,
const Http::ResponseHeaderMap&,
const Http::ResponseTrailerMap&,
const StreamInfo::StreamInfo& stream_info,
absl::string_view) const {
const auto time_field = (*time_field_extractor_)(stream_info);
if (!time_field.has_value()) {
return absl::nullopt;
}
if (date_formatter_.formatString().empty()) {
return AccessLogDateTimeFormatter::fromTime(stream_info.startTime());
return AccessLogDateTimeFormatter::fromTime(time_field.value());
}
return date_formatter_.fromTime(stream_info.startTime());
return date_formatter_.fromTime(time_field.value());
}

ProtobufWkt::Value StartTimeFormatter::formatValue(
ProtobufWkt::Value SystemTimeFormatter::formatValue(
const Http::RequestHeaderMap& request_headers, const Http::ResponseHeaderMap& response_headers,
const Http::ResponseTrailerMap& response_trailers, const StreamInfo::StreamInfo& stream_info,
absl::string_view local_reply_body) const {
Expand Down
40 changes: 37 additions & 3 deletions source/common/formatter/substitution_formatter.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ class SubstitutionFormatParser {
static const size_t RespParamStart{sizeof("RESP(") - 1};
static const size_t TrailParamStart{sizeof("TRAILER(") - 1};
static const size_t StartTimeParamStart{sizeof("START_TIME(") - 1};
static const size_t DownstreamPeerCertVStartParamStart{sizeof("DOWNSTREAM_PEER_CERT_V_START(") -
1};
static const size_t DownstreamPeerCertVEndParamStart{sizeof("DOWNSTREAM_PEER_CERT_V_END(") - 1};
};

/**
Expand Down Expand Up @@ -379,11 +382,15 @@ class FilterStateFormatter : public FormatterProvider {
};

/**
* FormatterProvider for request start time from StreamInfo.
* Base FormatterProvider for system times from StreamInfo.
*/
class StartTimeFormatter : public FormatterProvider {
class SystemTimeFormatter : public FormatterProvider {
public:
StartTimeFormatter(const std::string& format);
using TimeFieldExtractor =
std::function<absl::optional<SystemTime>(const StreamInfo::StreamInfo& stream_info)>;
using TimeFieldExtractorPtr = std::unique_ptr<TimeFieldExtractor>;

SystemTimeFormatter(const std::string& format, TimeFieldExtractorPtr f);

// FormatterProvider
absl::optional<std::string> format(const Http::RequestHeaderMap&, const Http::ResponseHeaderMap&,
Expand All @@ -395,6 +402,33 @@ class StartTimeFormatter : public FormatterProvider {

private:
const Envoy::DateFormatter date_formatter_;
const TimeFieldExtractorPtr time_field_extractor_;
};

/**
* SystemTimeFormatter (FormatterProvider) for request start time from StreamInfo.
*/
class StartTimeFormatter : public SystemTimeFormatter {
public:
StartTimeFormatter(const std::string& format);
};

/**
* SystemTimeFormatter (FormatterProvider) for downstream cert start time from the StreamInfo's
* ConnectionInfo.
*/
class DownstreamPeerCertVStartFormatter : public SystemTimeFormatter {
public:
DownstreamPeerCertVStartFormatter(const std::string& format);
};

/**
* SystemTimeFormatter (FormatterProvider) for downstream cert end time from the StreamInfo's
* ConnectionInfo.
*/
class DownstreamPeerCertVEndFormatter : public SystemTimeFormatter {
public:
DownstreamPeerCertVEndFormatter(const std::string& format);
};

} // namespace Formatter
Expand Down
Loading