From 62da9c077915c6e554ef6e8b79bc35fd2a70cc50 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Fri, 21 May 2021 15:37:46 +0100 Subject: [PATCH] Fix case sensitivity issues - Web hook method must be exactly "OPTIONS" - Kafka header keys are case sensitive Fixes #90 Signed-off-by: Jon Skeet --- .../KafkaClientExtensions.cs | 25 ++++++++----------- .../Http/HttpClientExtensions.cs | 2 +- .../Http/HttpListenerExtensions.cs | 2 +- .../Kafka/KafkaTest.cs | 19 ++++++++++++++ 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/CloudNative.CloudEvents.Kafka/KafkaClientExtensions.cs b/src/CloudNative.CloudEvents.Kafka/KafkaClientExtensions.cs index 7d72c46..419f821 100644 --- a/src/CloudNative.CloudEvents.Kafka/KafkaClientExtensions.cs +++ b/src/CloudNative.CloudEvents.Kafka/KafkaClientExtensions.cs @@ -23,8 +23,6 @@ namespace CloudNative.CloudEvents.Kafka private const string KafkaContentTypeAttributeName = "content-type"; private const string SpecVersionKafkaHeader = KafkaHeaderPrefix + "specversion"; - // TODO: Avoid all the byte[] -> string conversions? If we didn't care about case-sensitivity, we could prepare byte arrays to perform comparisons with. - /// /// Indicates whether this message holds a single CloudEvent. /// @@ -35,7 +33,7 @@ namespace CloudNative.CloudEvents.Kafka /// true, if the request is a CloudEvent public static bool IsCloudEvent(this Message message) => GetHeaderValue(message, SpecVersionKafkaHeader) is object || - MimeUtilities.IsCloudEventsContentType(ExtractContentType(message)); + MimeUtilities.IsCloudEventsContentType(GetHeaderValue(message, KafkaContentTypeAttributeName)); /// /// Converts this Kafka message into a CloudEvent object. @@ -66,7 +64,7 @@ namespace CloudNative.CloudEvents.Kafka throw new InvalidOperationException(); } - var contentType = ExtractContentType(message); + var contentType = GetHeaderValue(message, KafkaContentTypeAttributeName); CloudEvent cloudEvent; @@ -78,11 +76,10 @@ namespace CloudNative.CloudEvents.Kafka else { // Binary mode - if (!(GetHeaderValue(message, SpecVersionKafkaHeader) is byte[] versionIdBytes)) + if (!(GetHeaderValue(message, SpecVersionKafkaHeader) is string versionId)) { throw new ArgumentException("Request is not a CloudEvent"); } - string versionId = Encoding.UTF8.GetString(versionIdBytes); CloudEventsSpecVersion version = CloudEventsSpecVersion.FromVersionId(versionId) ?? throw new ArgumentException($"Unknown CloudEvents spec version '{versionId}'", nameof(message)); @@ -115,12 +112,6 @@ namespace CloudNative.CloudEvents.Kafka return Validation.CheckCloudEventArgument(cloudEvent, nameof(message)); } - private static string ExtractContentType(Message message) - { - var headerValue = GetHeaderValue(message, KafkaContentTypeAttributeName); - return headerValue is null ? null : Encoding.UTF8.GetString(headerValue); - } - private static void InitPartitioningKey(Message message, CloudEvent cloudEvent) { if (!string.IsNullOrEmpty(message.Key)) @@ -129,9 +120,13 @@ namespace CloudNative.CloudEvents.Kafka } } - private static byte[] GetHeaderValue(MessageMetadata message, string headerName) => - message.Headers.FirstOrDefault(x => string.Equals(x.Key, headerName, StringComparison.InvariantCultureIgnoreCase)) - ?.GetValueBytes(); + /// + /// Returns the last header value with the given name, decoded using UTF-8, or null if there is no such header. + /// + private static string GetHeaderValue(MessageMetadata message, string headerName) => + Validation.CheckNotNull(message, nameof(message)).Headers is null + ? null + : message.Headers.TryGetLastBytes(headerName, out var bytes) ? Encoding.UTF8.GetString(bytes) : null; /// /// Converts a CloudEvent to a Kafka message. diff --git a/src/CloudNative.CloudEvents/Http/HttpClientExtensions.cs b/src/CloudNative.CloudEvents/Http/HttpClientExtensions.cs index a9a1751..75ddab4 100644 --- a/src/CloudNative.CloudEvents/Http/HttpClientExtensions.cs +++ b/src/CloudNative.CloudEvents/Http/HttpClientExtensions.cs @@ -108,7 +108,7 @@ namespace CloudNative.CloudEvents.Http /// Indicates whether this HttpListenerRequest is a web hook validation request /// public static bool IsWebHookValidationRequest(this HttpRequestMessage httpRequestMessage) => - httpRequestMessage.Method.Method.Equals("options", StringComparison.InvariantCultureIgnoreCase) && + httpRequestMessage.Method.Method == "OPTIONS" && httpRequestMessage.Headers.Contains("WebHook-Request-Origin"); /// diff --git a/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs b/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs index dfcd52c..c658bb8 100644 --- a/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs +++ b/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs @@ -140,7 +140,7 @@ namespace CloudNative.CloudEvents.Http /// Indicates whether this HttpListenerRequest is a web hook validation request /// public static bool IsWebHookValidationRequest(this HttpListenerRequest httpRequestMessage) => - httpRequestMessage.HttpMethod.Equals("options", StringComparison.InvariantCultureIgnoreCase) && + httpRequestMessage.HttpMethod == "OPTIONS" && httpRequestMessage.Headers["WebHook-Request-Origin"] is object; /// diff --git a/test/CloudNative.CloudEvents.UnitTests/Kafka/KafkaTest.cs b/test/CloudNative.CloudEvents.UnitTests/Kafka/KafkaTest.cs index 9340d61..a730535 100644 --- a/test/CloudNative.CloudEvents.UnitTests/Kafka/KafkaTest.cs +++ b/test/CloudNative.CloudEvents.UnitTests/Kafka/KafkaTest.cs @@ -17,6 +17,25 @@ namespace CloudNative.CloudEvents.Kafka.UnitTests { public class KafkaTest { + [Theory] + [InlineData("content-type", "application/cloudevents", true)] + [InlineData("content-type", "APPLICATION/CLOUDEVENTS", true)] + [InlineData("CONTENT-TYPE", "application/cloudevents", false)] + [InlineData("ce_specversion", "1.0", true)] + [InlineData("CE_SPECVERSION", "1.0", false)] + public void IsCloudEvent(string headerName, string headerValue, bool expectedResult) + { + var message = new Message + { + Headers = new Headers { { headerName, Encoding.UTF8.GetBytes(headerValue) } } + }; + Assert.Equal(expectedResult, message.IsCloudEvent()); + } + + [Fact] + public void IsCloudEvent_NoHeaders() => + Assert.False(new Message().IsCloudEvent()); + [Fact] public void KafkaStructuredMessageTest() {