diff --git a/WebPush.Test/WebPushClientTest.cs b/WebPush.Test/WebPushClientTest.cs index ceffd2e..b271a6a 100644 --- a/WebPush.Test/WebPushClientTest.cs +++ b/WebPush.Test/WebPushClientTest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; @@ -36,6 +37,35 @@ public void InitializeTest() client = new WebPushClient(httpMessageHandlerMock.ToHttpClient()); } + [TestMethod] + public void TestBogusEndpoint() + { + var subscription = new PushSubscription("this is not a valid endpoint", TestPublicKey, TestPrivateKey); + Assert.ThrowsExactly(() => client.GenerateRequestDetails(subscription, @"test payload")); + } + + + [TestMethod] + [DataRow(TestPublicKey, "")] + [DataRow("", TestPrivateKey)] + [DataRow("", "")] + public void TestMissingAuthWithPayload(string publicKey, string privateKey) + { + var subscription = new PushSubscription(TestFcmEndpoint, publicKey, privateKey); + Assert.ThrowsExactly(() => client.GenerateRequestDetails(subscription, @"test payload")); + } + + [TestMethod] + [DataRow(TestPublicKey, "")] + [DataRow("", TestPrivateKey)] + [DataRow("", "")] + public void TestMissingAuthWithoutPayload(string publicKey, string privateKey) + { + var subscription = new PushSubscription(TestFcmEndpoint, publicKey, privateKey); + var message = client.GenerateRequestDetails(subscription, null); + Assert.IsNotNull(message); + } + [TestMethod] public void TestSetTopic() { @@ -45,12 +75,28 @@ public void TestSetTopic() } [TestMethod] - public void TestSetTopicFailures() + [DataRow("failing topic #3")] + [DataRow("")] + [DataRow("a123456789012345678901234567890toolong")] + public void TestSetTopicFailures(string topic) { var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey); - Assert.ThrowsExactly(() => client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions { Topic = "failing topic #3" })); + Assert.ThrowsExactly(() => client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions { Topic = topic, })); } + [TestMethod] + public void TestExtraHeaders() + { + var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey); + Dictionary extraHeaders = []; + extraHeaders["DEBUG-VERBOSE"] = true; + var message = client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions { ExtraHeaders = extraHeaders, }); + var checkHeader = message.Headers.GetValues(@"DEBUG-VERBOSE").First(); + Assert.IsNotNull(checkHeader); + Assert.AreEqual(@"True", checkHeader); + } + + [TestMethod] public void TestSetUrgency() @@ -61,15 +107,29 @@ public void TestSetUrgency() } [TestMethod] - public void TestSetContentEncoding() + [DataRow(ContentEncoding.Aes128gcm, "aes128gcm")] + [DataRow(ContentEncoding.Aesgcm, "aesgcm")] + public void TestSetContentEncoding(ContentEncoding encoding, string encodingHeaderValue) { var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey); - var messageAes128gcm = client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions { ContentEncoding = ContentEncoding.Aes128gcm }); - Assert.AreEqual(@"aes128gcm", messageAes128gcm.Content.Headers.ContentEncoding.First()); - var messageAesgcm = client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions { ContentEncoding = ContentEncoding.Aesgcm }); - Assert.AreEqual(@"aesgcm", messageAesgcm.Content.Headers.ContentEncoding.First()); + var message = client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions { ContentEncoding = encoding, }); + Assert.AreEqual(encodingHeaderValue, message.Content.Headers.ContentEncoding.First()); } + [TestMethod] + [DataRow(ContentEncoding.Aes128gcm, "aes128gcm")] + [DataRow(ContentEncoding.Aesgcm, "aesgcm")] + public void TestSetContentEncodingWithVapid(ContentEncoding encoding, string encodingHeaderValue) + { + client.SetVapidDetails(TestSubject, TestPublicKey, TestPrivateKey); + var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey); + var message = client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions { ContentEncoding = encoding, }); + Assert.AreEqual(encodingHeaderValue, message.Content.Headers.ContentEncoding.First()); + // var authorizationHeader = message.Headers.GetValues(@"Authorization").First(); + // Assert.StartsWith(@"vapid ", authorizationHeader); + } + + [TestMethod] public void TestSetVapidDetails() diff --git a/WebPush/Model/EncryptionResult.cs b/WebPush/Model/EncryptionResult.cs index 2fed814..e9a096e 100644 --- a/WebPush/Model/EncryptionResult.cs +++ b/WebPush/Model/EncryptionResult.cs @@ -2,9 +2,7 @@ namespace WebPush.Model; -// @LogicSoftware -// Originally From: https://github.com/LogicSoftware/WebPushEncryption/blob/master/src/EncryptionResult.cs -public class EncryptionResult +public sealed class EncryptionResult { public required byte[] PublicKey { get; set; } public required byte[] Payload { get; set; } diff --git a/WebPush/Model/InvalidEncryptionDetailsException.cs b/WebPush/Model/InvalidEncryptionDetailsException.cs index dd46852..ff71d1b 100644 --- a/WebPush/Model/InvalidEncryptionDetailsException.cs +++ b/WebPush/Model/InvalidEncryptionDetailsException.cs @@ -2,7 +2,7 @@ namespace WebPush.Model; -public class InvalidEncryptionDetailsException : Exception +public sealed class InvalidEncryptionDetailsException : Exception { public InvalidEncryptionDetailsException(string message, PushSubscription pushSubscription) : base(message) diff --git a/WebPush/Model/PushSubscription.cs b/WebPush/Model/PushSubscription.cs index 8816f73..a5d2e4e 100644 --- a/WebPush/Model/PushSubscription.cs +++ b/WebPush/Model/PushSubscription.cs @@ -1,6 +1,6 @@ namespace WebPush; -public class PushSubscription +public sealed class PushSubscription { public PushSubscription(string endpoint, string p256dh, string auth) { diff --git a/WebPush/Model/VapidDetails.cs b/WebPush/Model/VapidDetails.cs index 82e5e89..860ced5 100644 --- a/WebPush/Model/VapidDetails.cs +++ b/WebPush/Model/VapidDetails.cs @@ -3,7 +3,7 @@ namespace WebPush; -public class VapidDetails +public sealed class VapidDetails { /// This should be a URL or a 'mailto:' email address /// The VAPID public key as a base64 encoded string diff --git a/WebPush/Model/WebPushException.cs b/WebPush/Model/WebPushException.cs index 86b977d..29562f4 100644 --- a/WebPush/Model/WebPushException.cs +++ b/WebPush/Model/WebPushException.cs @@ -5,7 +5,7 @@ namespace WebPush.Model; -public class WebPushException : Exception +public sealed class WebPushException : Exception { public WebPushException(string message, PushSubscription pushSubscription, HttpResponseMessage responseMessage) : base(message) { diff --git a/WebPush/Model/WebPushOptions.cs b/WebPush/Model/WebPushOptions.cs index 3b9cbda..a5bcbdd 100644 --- a/WebPush/Model/WebPushOptions.cs +++ b/WebPush/Model/WebPushOptions.cs @@ -2,7 +2,7 @@ namespace WebPush; -public class WebPushOptions +public sealed class WebPushOptions { public VapidDetails? VapidDetails { get; set; } public const int DefaultTtl = 2419200; // default is 4 weeks diff --git a/WebPush/Util/EnumHelper.cs b/WebPush/Util/EnumHelper.cs index 50daf0e..eb914b1 100644 --- a/WebPush/Util/EnumHelper.cs +++ b/WebPush/Util/EnumHelper.cs @@ -4,7 +4,7 @@ namespace WebPush.Util; -public static partial class EnumHelper +internal static partial class EnumHelper { public static string ToKebabCaseLower(this T val) where T : Enum diff --git a/WebPush/VapidHelper.cs b/WebPush/VapidHelper.cs index 10b77ac..5eb8c74 100644 --- a/WebPush/VapidHelper.cs +++ b/WebPush/VapidHelper.cs @@ -18,6 +18,19 @@ public static VapidDetails GenerateVapidKeys() return new VapidDetails("", keys.GetEncodedPublicKey(), keys.GetEncodedPrivateKey()); } + /// + /// This method takes the required VAPID parameters and returns the required + /// header to be added to a Web Push Protocol Request. + /// + /// This must be the origin of the push service. + /// The VAPID details + /// The content encoding (defaults to Aes128gcm) + /// A dictionary of header key/value pairs. + public static Dictionary GetVapidHeaders(string audience, VapidDetails vapid, ContentEncoding contentEncoding = ContentEncoding.Aes128gcm) + { + return GetVapidHeaders(audience, vapid.Subject, vapid.PublicKey, vapid.PrivateKey, vapid.Expiration, contentEncoding); + } + /// /// This method takes the required VAPID parameters and returns the required /// header to be added to a Web Push Protocol Request. @@ -27,6 +40,7 @@ public static VapidDetails GenerateVapidKeys() /// The VAPID public key as a base64 encoded string /// The VAPID private key as a base64 encoded string /// The expiration of the VAPID JWT. + /// The content encoding (defaults to Aes128gcm) /// A dictionary of header key/value pairs. public static Dictionary GetVapidHeaders(string audience, string subject, string publicKey, string privateKey, DateTime? expiration = null, ContentEncoding contentEncoding = ContentEncoding.Aes128gcm) { diff --git a/WebPush/WebPushClient.cs b/WebPush/WebPushClient.cs index 651c51e..1cae1bc 100644 --- a/WebPush/WebPushClient.cs +++ b/WebPush/WebPushClient.cs @@ -105,7 +105,7 @@ public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string.IsNullOrEmpty(subscription.P256DH))) { throw new ArgumentException( - @"To send a message with a payload, the subscription must have 'auth' and 'p256dh' keys.", nameof(subscription)); + @"Unable to send a message with payload to this subscription since it doesn't have the required encryption key", nameof(subscription)); } if (options is not null) @@ -145,12 +145,6 @@ public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, var contentEncoding = options?.ContentEncoding ?? WebPushOptions.DefaultContentEncoding; if (!string.IsNullOrEmpty(payload)) { - if (string.IsNullOrEmpty(subscription.P256DH) || string.IsNullOrEmpty(subscription.Auth)) - { - throw new ArgumentException( - @"Unable to send a message with payload to this subscription since it doesn't have the required encryption key", nameof(subscription)); - } - var encryptedPayload = EncryptPayload(subscription, payload); request.Content = new ByteArrayContent(encryptedPayload.Payload); @@ -174,7 +168,7 @@ public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, { var uri = new Uri(subscription.Endpoint); var audience = uri.Scheme + @"://" + uri.Host; - var vapidHeaders = VapidHelper.GetVapidHeaders(audience, vapidDetails.Subject, vapidDetails.PublicKey, vapidDetails.PrivateKey, vapidDetails.Expiration, contentEncoding); + var vapidHeaders = VapidHelper.GetVapidHeaders(audience, vapidDetails, contentEncoding); request.Headers.Add(@"Authorization", vapidHeaders["Authorization"]); if (contentEncoding == ContentEncoding.Aesgcm) {