diff --git a/.gitignore b/.gitignore
index 78f05bc..b9757b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
bin
obj
+artifacts
*.xslt.sql
*.user
*.suo
diff --git a/Directory.Build.props b/Directory.Build.props
index f479e98..2267414 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,6 +1,7 @@
true
+ true
diff --git a/WebPush.Test/ECKeyHelperTest.cs b/WebPush.Test/ECKeyHelperTest.cs
index ae2ef6d..cd03cec 100644
--- a/WebPush.Test/ECKeyHelperTest.cs
+++ b/WebPush.Test/ECKeyHelperTest.cs
@@ -1,69 +1,88 @@
using System.Linq;
+using Microsoft.IdentityModel.Tokens;
using Microsoft.VisualStudio.TestTools.UnitTesting;
-using Org.BouncyCastle.Crypto.Parameters;
using WebPush.Util;
-namespace WebPush.Test
+namespace WebPush.Test;
+
+[TestClass]
+public class ECKeyHelperTest
{
- [TestClass]
- public class ECKeyHelperTest
+ private const string TestPublicKey =
+ @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33yJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A";
+
+ private const string TestPrivateKey = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI";
+
+ [TestMethod]
+ public void TestGenerateKeys()
{
- private const string TestPublicKey =
- @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33yJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A";
+ var keypair = ECKeyHelper.GenerateKeys();
+ var publicKey = keypair.GetPublicKey();
+ var privateKey = keypair.GetPrivateKey();
- private const string TestPrivateKey = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI";
+ var publicKeyLength = publicKey.Length;
+ var privateKeyLength = privateKey.Length;
- [TestMethod]
- public void TestGenerateKeys()
- {
- var keys = ECKeyHelper.GenerateKeys();
+ Assert.AreEqual(65, publicKeyLength);
+ Assert.AreEqual(32, privateKeyLength);
+ }
+
+ [TestMethod]
+ public void TestGenerateKeysNoCache()
+ {
+ var keys1 = ECKeyHelper.GenerateKeys();
+ var keys2 = ECKeyHelper.GenerateKeys();
- var publicKey = ((ECPublicKeyParameters) keys.Public).Q.GetEncoded(false);
- var privateKey = ((ECPrivateKeyParameters) keys.Private).D.ToByteArrayUnsigned();
+ var publicKey1 = keys1.GetPublicKey();
+ var privateKey1 = keys1.GetPrivateKey();
- var publicKeyLength = publicKey.Length;
- var privateKeyLength = privateKey.Length;
+ var publicKey2 = keys2.GetPublicKey();
+ var privateKey2 = keys2.GetPrivateKey();
- Assert.AreEqual(65, publicKeyLength);
- Assert.AreEqual(32, privateKeyLength);
- }
+ Assert.IsFalse(publicKey1.SequenceEqual(publicKey2));
+ Assert.IsFalse(privateKey1.SequenceEqual(privateKey2));
+ }
- [TestMethod]
- public void TestGenerateKeysNoCache()
- {
- var keys1 = ECKeyHelper.GenerateKeys();
- var keys2 = ECKeyHelper.GenerateKeys();
+ [TestMethod]
+ public void TestGetPrivateKey()
+ {
+ var privateKey = Base64UrlEncoder.DecodeBytes(TestPrivateKey);
+ var publicKey = Base64UrlEncoder.DecodeBytes(TestPublicKey);
+ var keypair = ECKeyHelper.GetKeyPair(privateKey, publicKey);
- var publicKey1 = ((ECPublicKeyParameters) keys1.Public).Q.GetEncoded(false);
- var privateKey1 = ((ECPrivateKeyParameters) keys1.Private).D.ToByteArrayUnsigned();
+ var importedPrivateKey = keypair.GetEncodedPrivateKey();
+ Assert.AreEqual(TestPrivateKey, importedPrivateKey);
- var publicKey2 = ((ECPublicKeyParameters) keys2.Public).Q.GetEncoded(false);
- var privateKey2 = ((ECPrivateKeyParameters) keys2.Private).D.ToByteArrayUnsigned();
+ var rawPrivateKey = keypair.GetPrivateKey();
+ Assert.IsTrue(privateKey.SequenceEqual(rawPrivateKey));
+ }
- Assert.IsFalse(publicKey1.SequenceEqual(publicKey2));
- Assert.IsFalse(privateKey1.SequenceEqual(privateKey2));
- }
+ [TestMethod]
+ public void TestGetPublicKey()
+ {
+ var privateKey = Base64UrlEncoder.DecodeBytes(TestPrivateKey);
+ var publicKey = Base64UrlEncoder.DecodeBytes(TestPublicKey);
+ var keypair = ECKeyHelper.GetKeyPair(privateKey, publicKey);
- [TestMethod]
- public void TestGetPrivateKey()
- {
- var privateKey = UrlBase64.Decode(TestPrivateKey);
- var privateKeyParams = ECKeyHelper.GetPrivateKey(privateKey);
+ var importedPublicKey = keypair.GetEncodedPublicKey();
+ Assert.AreEqual(TestPublicKey, importedPublicKey);
- var importedPrivateKey = UrlBase64.Encode(privateKeyParams.D.ToByteArrayUnsigned());
+ var rawPublicKey = keypair.GetPublicKey();
+ Assert.IsTrue(publicKey.SequenceEqual(rawPublicKey));
+ }
- Assert.AreEqual(TestPrivateKey, importedPrivateKey);
- }
+ [TestMethod]
+ public void TestGetKeyPairNet()
+ {
+ var privateKey = Base64UrlEncoder.DecodeBytes(TestPrivateKey);
+ var publicKey = Base64UrlEncoder.DecodeBytes(TestPublicKey);
- [TestMethod]
- public void TestGetPublicKey()
- {
- var publicKey = UrlBase64.Decode(TestPublicKey);
- var publicKeyParams = ECKeyHelper.GetPublicKey(publicKey);
+ var keypair = ECKeyHelper.GetKeyPair(privateKey, publicKey);
- var importedPublicKey = UrlBase64.Encode(publicKeyParams.Q.GetEncoded(false));
+ var importedPublicKey = keypair.GetEncodedPublicKey();
+ Assert.AreEqual(TestPublicKey, importedPublicKey);
- Assert.AreEqual(TestPublicKey, importedPublicKey);
- }
+ var importedPrivateKey = keypair.GetEncodedPrivateKey();
+ Assert.AreEqual(TestPrivateKey, importedPrivateKey);
}
-}
\ No newline at end of file
+}
diff --git a/WebPush.Test/JWSSignerTest.cs b/WebPush.Test/JWSSignerTest.cs
index 184d755..1cae0ac 100644
--- a/WebPush.Test/JWSSignerTest.cs
+++ b/WebPush.Test/JWSSignerTest.cs
@@ -1,52 +1,63 @@
-using System.Collections.Generic;
+using System;
+using System.Security.Claims;
using System.Text;
+using Microsoft.IdentityModel.JsonWebTokens;
+using Microsoft.IdentityModel.Tokens;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using WebPush.Util;
-namespace WebPush.Test
-{
- [TestClass]
- public class JWSSignerTest
- {
- private const string TestPrivateKey = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI";
-
- [TestMethod]
- public void TestGenerateSignature()
- {
- var decodedPrivateKey = UrlBase64.Decode(TestPrivateKey);
- var privateKey = ECKeyHelper.GetPrivateKey(decodedPrivateKey);
-
- var header = new Dictionary();
- header.Add("typ", "JWT");
- header.Add("alg", "ES256");
-
- var jwtPayload = new Dictionary();
- jwtPayload.Add("aud", "aud");
- jwtPayload.Add("exp", 1);
- jwtPayload.Add("sub", "subject");
-
- var signer = new JwsSigner(privateKey);
- var token = signer.GenerateSignature(header, jwtPayload);
+namespace WebPush.Test;
- var tokenParts = token.Split('.');
-
- Assert.AreEqual(3, tokenParts.Length);
-
- var encodedHeader = tokenParts[0];
- var encodedPayload = tokenParts[1];
- var signature = tokenParts[2];
+[TestClass]
+public class JWSSignerTest
+{
+ private const string TestPublicKey =
+ @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33yJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A";
- var decodedHeader = Encoding.UTF8.GetString(UrlBase64.Decode(encodedHeader));
- var decodedPayload = Encoding.UTF8.GetString(UrlBase64.Decode(encodedPayload));
+ private const string TestPrivateKey = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI";
- Assert.AreEqual(@"{""typ"":""JWT"",""alg"":""ES256""}", decodedHeader);
- Assert.AreEqual(@"{""aud"":""aud"",""exp"":1,""sub"":""subject""}", decodedPayload);
- var decodedSignature = UrlBase64.Decode(signature);
- var decodedSignatureLength = decodedSignature.Length;
+ [TestMethod]
+ public void TestGenerateSignature()
+ {
+ var key = ECKeyHelper.GetKeyPair(Base64UrlEncoder.DecodeBytes(TestPrivateKey), Base64UrlEncoder.DecodeBytes(TestPublicKey));
+ var handler = new JsonWebTokenHandler
+ {
+ SetDefaultTimesOnTokenCreation = false
+ };
+ var now = DateTime.UtcNow;
+ var epoch = new DateTime(1970, 1, 1, 0, 0, 1, DateTimeKind.Utc);
+ var subject = new ClaimsIdentity([new Claim(JwtRegisteredClaimNames.Sub, "subject")]);
- var isSignatureLengthValid = decodedSignatureLength == 66 || decodedSignatureLength == 64;
- Assert.AreEqual(true, isSignatureLengthValid);
- }
+ string token = handler.CreateToken(new SecurityTokenDescriptor
+ {
+ Audience = "aud",
+ // Expires = now.AddMinutes(1),
+ Expires = epoch,
+ // IssuedAt = now,
+ Subject = subject,
+ SigningCredentials = new SigningCredentials(new ECDsaSecurityKey(key), SecurityAlgorithms.EcdsaSha256),
+ });
+ var tokenParts = token.Split('.');
+
+ Assert.HasCount(3, tokenParts);
+
+ var encodedHeader = tokenParts[0];
+ var encodedPayload = tokenParts[1];
+ var signature = tokenParts[2];
+
+ var decodedHeader = Encoding.UTF8.GetString(Base64UrlEncoder.DecodeBytes(encodedHeader));
+ var decodedPayload = Encoding.UTF8.GetString(Base64UrlEncoder.DecodeBytes(encodedPayload));
+
+ Assert.AreEqual(@"{""alg"":""ES256"",""typ"":""JWT""}", decodedHeader);
+ Assert.Contains(@"""typ"":""JWT""", decodedHeader);
+ Assert.Contains(@"""alg"":""ES256""", decodedHeader);
+ Assert.AreEqual(@"{""aud"":""aud"",""exp"":1,""sub"":""subject""}", decodedPayload);
+
+ var decodedSignature = Base64UrlEncoder.DecodeBytes(signature);
+ var decodedSignatureLength = decodedSignature.Length;
+
+ var isSignatureLengthValid = decodedSignatureLength == 66 || decodedSignatureLength == 64;
+ Assert.IsTrue(isSignatureLengthValid);
}
-}
\ No newline at end of file
+}
diff --git a/WebPush.Test/UrlBase64Test.cs b/WebPush.Test/UrlBase64Test.cs
index 5636517..e351daa 100644
--- a/WebPush.Test/UrlBase64Test.cs
+++ b/WebPush.Test/UrlBase64Test.cs
@@ -1,26 +1,27 @@
using System.Linq;
+using Microsoft.IdentityModel.Tokens;
using Microsoft.VisualStudio.TestTools.UnitTesting;
-using WebPush.Util;
-namespace WebPush.Test
+[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
+
+namespace WebPush.Test;
+
+[TestClass]
+public class UrlBase64Test
{
- [TestClass]
- public class UrlBase64Test
+ [TestMethod]
+ public void TestBase64UrlDecode()
{
- [TestMethod]
- public void TestBase64UrlDecode()
- {
- var expected = new byte[3] {181, 235, 45};
- var actual = UrlBase64.Decode(@"test");
- Assert.IsTrue(actual.SequenceEqual(expected));
- }
+ var expected = new byte[3] { 181, 235, 45 };
+ var actual = Base64UrlEncoder.DecodeBytes(@"test");
+ Assert.IsTrue(actual.SequenceEqual(expected));
+ }
- [TestMethod]
- public void TestBase64UrlEncode()
- {
- var expected = @"test";
- var actual = UrlBase64.Encode(new byte[3] {181, 235, 45});
- Assert.AreEqual(expected, actual);
- }
+ [TestMethod]
+ public void TestBase64UrlEncode()
+ {
+ var expected = @"test";
+ var actual = Base64UrlEncoder.Encode([181, 235, 45]);
+ Assert.AreEqual(expected, actual);
}
-}
\ No newline at end of file
+}
diff --git a/WebPush.Test/VapidHelperTest.cs b/WebPush.Test/VapidHelperTest.cs
index 73ba0e6..028d5e5 100644
--- a/WebPush.Test/VapidHelperTest.cs
+++ b/WebPush.Test/VapidHelperTest.cs
@@ -1,119 +1,217 @@
using System;
+using System.Security.Cryptography;
+using System.Text;
+using Microsoft.IdentityModel.Tokens;
using Microsoft.VisualStudio.TestTools.UnitTesting;
-using WebPush.Util;
+using WebPush.Model;
-namespace WebPush.Test
+namespace WebPush.Test;
+
+[TestClass]
+public class VapidHelperTest
{
- [TestClass]
- public class VapidHelperTest
- {
- private const string ValidAudience = @"http://example.com";
- private const string ValidSubject = @"http://example.com/example";
- private const string ValidSubjectMailto = @"mailto:example@example.com";
-
- private const string TestPublicKey =
- @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33yJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A";
-
- private const string TestPrivateKey = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI";
-
- [TestMethod]
- public void TestGenerateVapidKeys()
- {
- var keys = VapidHelper.GenerateVapidKeys();
- var publicKey = UrlBase64.Decode(keys.PublicKey);
- var privateKey = UrlBase64.Decode(keys.PrivateKey);
-
- Assert.AreEqual(32, privateKey.Length);
- Assert.AreEqual(65, publicKey.Length);
- }
-
- [TestMethod]
- public void TestGenerateVapidKeysNoCache()
- {
- var keys1 = VapidHelper.GenerateVapidKeys();
- var keys2 = VapidHelper.GenerateVapidKeys();
-
- Assert.AreNotEqual(keys1.PublicKey, keys2.PublicKey);
- Assert.AreNotEqual(keys1.PrivateKey, keys2.PrivateKey);
- }
-
- [TestMethod]
- public void TestGetVapidHeaders()
- {
- var publicKey = TestPublicKey;
- var privateKey = TestPrivateKey;
- var headers = VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, publicKey, privateKey);
-
- Assert.IsTrue(headers.ContainsKey(@"Authorization"));
- Assert.IsTrue(headers.ContainsKey(@"Crypto-Key"));
- }
-
- [TestMethod]
- public void TestGetVapidHeadersAudienceNotAUrl()
- {
- var publicKey = TestPublicKey;
- var privateKey = TestPrivateKey;
- Assert.Throws(
- delegate
- {
- VapidHelper.GetVapidHeaders("invalid audience", ValidSubjectMailto, publicKey, privateKey);
- });
- }
-
- [TestMethod]
- public void TestGetVapidHeadersInvalidPrivateKey()
- {
- var publicKey = UrlBase64.Encode(new byte[65]);
- var privateKey = UrlBase64.Encode(new byte[1]);
-
- Assert.Throws(
- delegate { VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, publicKey, privateKey); });
- }
-
- [TestMethod]
- public void TestGetVapidHeadersInvalidPublicKey()
- {
- var publicKey = UrlBase64.Encode(new byte[1]);
- var privateKey = UrlBase64.Encode(new byte[32]);
-
- Assert.Throws(
- delegate { VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, publicKey, privateKey); });
- }
-
- [TestMethod]
- public void TestGetVapidHeadersSubjectNotAUrlOrMailTo()
- {
- var publicKey = TestPublicKey;
- var privateKey = TestPrivateKey;
-
- Assert.Throws(
- delegate { VapidHelper.GetVapidHeaders(ValidAudience, @"invalid subject", publicKey, privateKey); });
- }
-
- [TestMethod]
- public void TestGetVapidHeadersWithMailToSubject()
- {
- var publicKey = TestPublicKey;
- var privateKey = TestPrivateKey;
- var headers = VapidHelper.GetVapidHeaders(ValidAudience, ValidSubjectMailto, publicKey,
- privateKey);
-
- Assert.IsTrue(headers.ContainsKey(@"Authorization"));
- Assert.IsTrue(headers.ContainsKey(@"Crypto-Key"));
- }
-
- [TestMethod]
- public void TestExpirationInPastExceptions()
- {
- var publicKey = TestPublicKey;
- var privateKey = TestPrivateKey;
-
- Assert.Throws(
- delegate
- {
- VapidHelper.GetVapidHeaders(ValidAudience, ValidSubjectMailto, publicKey,
- privateKey, 1552715607);
- });
- }
- }
-}
\ No newline at end of file
+ private const string ValidAudience = @"http://example.com";
+ private const string ValidSubject = @"http://example.com/example";
+ private const string ValidSubjectMailto = @"mailto:example@example.com";
+
+ private const string TestPublicKey =
+ @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33yJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A";
+
+ private const string TestPrivateKey = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI";
+
+ [TestMethod]
+ public void TestGenerateVapidKeys()
+ {
+ var keys = VapidHelper.GenerateVapidKeys();
+ var publicKey = Base64UrlEncoder.DecodeBytes(keys.PublicKey);
+ var privateKey = Base64UrlEncoder.DecodeBytes(keys.PrivateKey);
+
+ Assert.HasCount(32, privateKey);
+ Assert.HasCount(65, publicKey);
+ }
+
+ [TestMethod]
+ public void TestGenerateVapidKeysNoCache()
+ {
+ var keys1 = VapidHelper.GenerateVapidKeys();
+ var keys2 = VapidHelper.GenerateVapidKeys();
+
+ Assert.AreNotEqual(keys1.PublicKey, keys2.PublicKey);
+ Assert.AreNotEqual(keys1.PrivateKey, keys2.PrivateKey);
+ }
+
+ [TestMethod]
+ public void TestGetVapidHeaders()
+ {
+ var publicKey = TestPublicKey;
+ var privateKey = TestPrivateKey;
+ var headers = VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, publicKey, privateKey);
+
+ Assert.IsTrue(headers.ContainsKey(@"Authorization"));
+ }
+
+ [TestMethod]
+ public void TestGetVapidHeadersAudienceNotAUrl()
+ {
+ var publicKey = TestPublicKey;
+ var privateKey = TestPrivateKey;
+ Assert.ThrowsExactly(
+ delegate
+ {
+ VapidHelper.GetVapidHeaders("invalid audience", ValidSubjectMailto, publicKey, privateKey);
+ });
+ }
+
+ [TestMethod]
+ public void TestGetVapidHeadersAudienceMissing()
+ {
+ var publicKey = TestPublicKey;
+ var privateKey = TestPrivateKey;
+ Assert.ThrowsExactly(
+ delegate
+ {
+ VapidHelper.GetVapidHeaders("", ValidSubjectMailto, publicKey, privateKey);
+ });
+ }
+
+
+ [TestMethod]
+ public void TestGetVapidHeadersInvalidPrivateKey()
+ {
+ var publicKey = Base64UrlEncoder.Encode(new byte[65]);
+ var privateKey = Base64UrlEncoder.Encode(new byte[1]);
+
+ Assert.ThrowsExactly(
+ delegate { VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, publicKey, privateKey); });
+ }
+
+ [TestMethod]
+ public void TestGetVapidHeadersInvalidPublicKey()
+ {
+ var publicKey = Base64UrlEncoder.Encode(new byte[1]);
+ var privateKey = Base64UrlEncoder.Encode(new byte[32]);
+
+ Assert.ThrowsExactly(
+ delegate { VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, publicKey, privateKey); });
+ }
+
+ [TestMethod]
+ public void TestGetVapidHeadersPublicKeyMissing()
+ {
+ Assert.ThrowsExactly(
+ delegate { VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, "", TestPrivateKey); });
+ }
+
+ [TestMethod]
+ public void TestGetVapidHeadersPublicKeyInvalidBase64()
+ {
+ Assert.Throws(
+ delegate { VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33zJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A", TestPrivateKey); });
+ }
+
+ [TestMethod]
+ public void TestGetVapidHeadersPrivateKeyMissing()
+ {
+ Assert.ThrowsExactly(
+ delegate { VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, TestPublicKey, ""); });
+ }
+
+ [TestMethod]
+ public void TestGetVapidHeadersPrivateKeyInvalidBase64()
+ {
+ Assert.Throws(
+ delegate { VapidHelper.GetVapidHeaders(ValidAudience, ValidSubject, TestPublicKey, @"WRONGKmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI"); });
+ }
+
+ [TestMethod]
+ public void TestGetVapidHeadersSubjectNotAUrlOrMailTo()
+ {
+ var publicKey = TestPublicKey;
+ var privateKey = TestPrivateKey;
+
+ Assert.ThrowsExactly(
+ delegate { VapidHelper.GetVapidHeaders(ValidAudience, @"invalid subject", publicKey, privateKey); });
+ }
+
+ [TestMethod]
+ public void TestGetVapidHeadersSubjectMissing()
+ {
+ var publicKey = TestPublicKey;
+ var privateKey = TestPrivateKey;
+
+ Assert.ThrowsExactly(
+ delegate { VapidHelper.GetVapidHeaders(ValidAudience, " ", publicKey, privateKey); });
+ }
+
+ [TestMethod]
+ public void TestGetVapidHeadersWithMailToSubject()
+ {
+ var publicKey = TestPublicKey;
+ var privateKey = TestPrivateKey;
+ var headers = VapidHelper.GetVapidHeaders(ValidAudience, ValidSubjectMailto, publicKey,
+ privateKey);
+
+ Assert.IsTrue(headers.ContainsKey(@"Authorization"));
+ }
+
+ [TestMethod]
+ public void TestExpirationInPastExceptions()
+ {
+ var publicKey = TestPublicKey;
+ var privateKey = TestPrivateKey;
+
+ Assert.ThrowsExactly(
+ delegate
+ {
+ VapidHelper.GetVapidHeaders(ValidAudience, ValidSubjectMailto, publicKey,
+ privateKey, DateTimeOffset.FromUnixTimeSeconds(1552715607).UtcDateTime);
+ });
+ }
+
+
+ [TestMethod]
+ public void TestVapidHeaders()
+ {
+ var vapidHeaders = VapidHelper.GetVapidHeaders(ValidAudience, ValidSubjectMailto, TestPublicKey, TestPrivateKey, new DateTime(2128, 08, 08, 08, 08, 08), ContentEncoding.Aes128gcm);
+
+ vapidHeaders.TryGetValue("Authorization", out var authHeader);
+ Assert.IsNotNull(vapidHeaders);
+
+ var partsSpace = authHeader.Split(' ');
+ Assert.IsGreaterThanOrEqualTo(3, partsSpace.Length);
+
+ var authType = partsSpace[0];
+ Assert.AreEqual("vapid", authType);
+
+ var jwkPart = partsSpace[1][0..^1]; // remove delimiter ','
+ Assert.StartsWith("t=", jwkPart);
+ var token = jwkPart["t=".Length..];
+ var tokenParts = token.Split('.');
+
+ Assert.HasCount(3, tokenParts);
+
+ var encodedHeader = tokenParts[0];
+ var encodedPayload = tokenParts[1];
+ var signature = tokenParts[2];
+
+ var decodedHeader = Encoding.UTF8.GetString(Base64UrlEncoder.DecodeBytes(encodedHeader));
+ var decodedPayload = Encoding.UTF8.GetString(Base64UrlEncoder.DecodeBytes(encodedPayload));
+
+ Assert.AreEqual(@"{""alg"":""ES256"",""typ"":""JWT""}", decodedHeader);
+ Assert.Contains(@"""typ"":""JWT""", decodedHeader);
+ Assert.Contains(@"""alg"":""ES256""", decodedHeader);
+ Assert.Contains(@$"""aud"":""{ValidAudience}""", decodedPayload);
+ Assert.Contains(@$"""sub"":""{ValidSubjectMailto}""", decodedPayload);
+ Assert.MatchesRegex(@"""exp"":\d+", decodedPayload);
+
+ var decodedSignature = Base64UrlEncoder.DecodeBytes(signature);
+ var decodedSignatureLength = decodedSignature.Length;
+
+ var isSignatureLengthValid = decodedSignatureLength == 66 || decodedSignatureLength == 64;
+ Assert.IsTrue(isSignatureLengthValid);
+
+ var keyPart = partsSpace[2];
+ Assert.StartsWith("k=", keyPart);
+ Assert.AreEqual(TestPublicKey, keyPart["k-".Length..]);
+ }
+}
diff --git a/WebPush.Test/WebPush.Test.csproj b/WebPush.Test/WebPush.Test.csproj
index b31f159..2fc9cd4 100755
--- a/WebPush.Test/WebPush.Test.csproj
+++ b/WebPush.Test/WebPush.Test.csproj
@@ -1,15 +1,16 @@
- net48;net8.0;net9.0;net10.0
+ net8.0;net9.0;net10.0
false
+ true
-
+
-
-
+
+
diff --git a/WebPush.Test/WebPushClientTest.cs b/WebPush.Test/WebPushClientTest.cs
index 5032d79..e556758 100644
--- a/WebPush.Test/WebPushClientTest.cs
+++ b/WebPush.Test/WebPushClientTest.cs
@@ -1,185 +1,143 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using RichardSzalay.MockHttp;
-using System;
-using System.Collections.Generic;
+using System;
using System.Linq;
using System.Net;
using System.Net.Http;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using RichardSzalay.MockHttp;
using WebPush.Model;
-namespace WebPush.Test
+namespace WebPush.Test;
+
+[TestClass]
+public class WebPushClientTest
{
- [TestClass]
- public class WebPushClientTest
+ private const string TestPublicKey =
+ @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33yJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A";
+
+ private const string TestPrivateKey = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI";
+
+ private const string TestGcmEndpoint = @"https://android.googleapis.com/gcm/send/";
+
+ private const string TestFcmEndpoint =
+ @"https://fcm.googleapis.com/fcm/send/efz_TLX_rLU:APA91bE6U0iybLYvv0F3mf6";
+
+ private const string TestFirefoxEndpoint =
+ @"https://updates.push.services.mozilla.com/wpush/v2/gBABAABgOe_sGrdrsT35ljtA4O9xCX";
+
+ public const string TestSubject = "mailto:example@example.com";
+
+ private MockHttpMessageHandler httpMessageHandlerMock;
+ private WebPushClient client;
+
+ [TestInitialize]
+ public void InitializeTest()
{
- private const string TestPublicKey =
- @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33yJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A";
-
- private const string TestPrivateKey = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI";
-
- private const string TestGcmEndpoint = @"https://android.googleapis.com/gcm/send/";
-
- private const string TestFcmEndpoint =
- @"https://fcm.googleapis.com/fcm/send/efz_TLX_rLU:APA91bE6U0iybLYvv0F3mf6";
-
- private const string TestFirefoxEndpoint =
- @"https://updates.push.services.mozilla.com/wpush/v2/gBABAABgOe_sGrdrsT35ljtA4O9xCX";
-
- public const string TestSubject = "mailto:example@example.com";
-
- private MockHttpMessageHandler httpMessageHandlerMock;
- private WebPushClient client;
-
- [TestInitialize]
- public void InitializeTest()
- {
- httpMessageHandlerMock = new MockHttpMessageHandler();
- client = new WebPushClient(httpMessageHandlerMock.ToHttpClient());
- }
-
- [TestMethod]
- public void TestGcmApiKeyInOptions()
- {
- var gcmAPIKey = @"teststring";
- var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey);
-
- var options = new Dictionary();
- options[@"gcmAPIKey"] = gcmAPIKey;
- var message = client.GenerateRequestDetails(subscription, @"test payload", options);
- var authorizationHeader = message.Headers.GetValues(@"Authorization").First();
-
- Assert.AreEqual("key=" + gcmAPIKey, authorizationHeader);
-
- // Test previous incorrect casing of gcmAPIKey
- var options2 = new Dictionary();
- options2[@"gcmApiKey"] = gcmAPIKey;
- Assert.Throws(delegate
- {
- client.GenerateRequestDetails(subscription, "test payload", options2);
- });
- }
-
- [TestMethod]
- public void TestSetGcmApiKey()
- {
- var gcmAPIKey = @"teststring";
- client.SetGcmApiKey(gcmAPIKey);
- var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey);
- var message = client.GenerateRequestDetails(subscription, @"test payload");
- var authorizationHeader = message.Headers.GetValues(@"Authorization").First();
-
- Assert.AreEqual(@"key=" + gcmAPIKey, authorizationHeader);
- }
-
- [TestMethod]
- public void TestSetGCMAPIKeyEmptyString()
- {
- Assert.Throws(delegate
- { client.SetGcmApiKey(""); });
- }
-
- [TestMethod]
- public void TestSetGcmApiKeyNonGcmPushService()
- {
- // Ensure that the API key doesn't get added on a service that doesn't accept it.
- var gcmAPIKey = @"teststring";
- client.SetGcmApiKey(gcmAPIKey);
- var subscription = new PushSubscription(TestFirefoxEndpoint, TestPublicKey, TestPrivateKey);
- var message = client.GenerateRequestDetails(subscription, @"test payload");
-
- Assert.IsFalse(message.Headers.TryGetValues(@"Authorization", out var values));
- }
-
- [TestMethod]
- public void TestSetGcmApiKeyNull()
- {
- client.SetGcmApiKey(@"somestring");
- client.SetGcmApiKey(null);
-
- var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey);
- var message = client.GenerateRequestDetails(subscription, @"test payload");
-
- Assert.IsFalse(message.Headers.TryGetValues("Authorization", out var values));
- }
-
- [TestMethod]
- public void TestSetVapidDetails()
- {
- client.SetVapidDetails(TestSubject, TestPublicKey, TestPrivateKey);
-
- var subscription = new PushSubscription(TestFirefoxEndpoint, TestPublicKey, TestPrivateKey);
- var message = client.GenerateRequestDetails(subscription, @"test payload");
- var authorizationHeader = message.Headers.GetValues(@"Authorization").First();
- var cryptoHeader = message.Headers.GetValues(@"Crypto-Key").First();
-
- Assert.IsTrue(authorizationHeader.StartsWith(@"WebPush "));
- Assert.IsTrue(cryptoHeader.Contains(@"p256ecdsa"));
- }
-
- [TestMethod]
- public void TestFcmAddsAuthorizationHeader()
- {
- client.SetGcmApiKey(@"somestring");
- var subscription = new PushSubscription(TestFcmEndpoint, TestPublicKey, TestPrivateKey);
- var message = client.GenerateRequestDetails(subscription, @"test payload");
- var authorizationHeader = message.Headers.GetValues(@"Authorization").First();
-
- Assert.IsTrue(authorizationHeader.StartsWith(@"key="));
- }
-
- [TestMethod]
- [DataRow(HttpStatusCode.Created)]
- [DataRow(HttpStatusCode.Accepted)]
- public void TestHandlingSuccessHttpCodes(HttpStatusCode status)
- {
- TestSendNotification(status);
- }
-
- [TestMethod]
- [DataRow(HttpStatusCode.BadRequest, "Bad Request")]
- [DataRow(HttpStatusCode.RequestEntityTooLarge, "Payload too large")]
- [DataRow((HttpStatusCode)429, "Too many request")]
- [DataRow(HttpStatusCode.NotFound, "Subscription no longer valid")]
- [DataRow(HttpStatusCode.Gone, "Subscription no longer valid")]
- [DataRow(HttpStatusCode.InternalServerError, "Received unexpected response code: 500")]
- public void TestHandlingFailureHttpCodes(HttpStatusCode status, string expectedMessage)
- {
- var actual = Assert.Throws(() => TestSendNotification(status));
- Assert.AreEqual(expectedMessage, actual.Message);
- }
-
- [TestMethod]
- [DataRow(HttpStatusCode.BadRequest, "authorization key missing", "Bad Request. Details: authorization key missing")]
- [DataRow(HttpStatusCode.RequestEntityTooLarge, "max size is 512", "Payload too large. Details: max size is 512")]
- [DataRow((HttpStatusCode)429, "the api is limited", "Too many request. Details: the api is limited")]
- [DataRow(HttpStatusCode.NotFound, "", "Subscription no longer valid")]
- [DataRow(HttpStatusCode.Gone, "", "Subscription no longer valid")]
- [DataRow(HttpStatusCode.InternalServerError, "internal error", "Received unexpected response code: 500. Details: internal error")]
- public void TestHandlingFailureMessages(HttpStatusCode status, string response, string expectedMessage)
- {
- var actual = Assert.Throws(() => TestSendNotification(status, response));
- Assert.AreEqual(expectedMessage, actual.Message);
- }
-
- [TestMethod]
- [DataRow(1)]
- [DataRow(5)]
- [DataRow(10)]
- [DataRow(50)]
- public void TestHandleInvalidPublicKeys(int charactersToDrop)
- {
- var invalidKey = TestPublicKey.Substring(0, TestPublicKey.Length - charactersToDrop);
-
- Assert.Throws(() => TestSendNotification(HttpStatusCode.OK, response: null, invalidKey));
- }
-
- private void TestSendNotification(HttpStatusCode status, string response = null, string publicKey = TestPublicKey)
- {
- var subscription = new PushSubscription(TestFcmEndpoint, publicKey, TestPrivateKey);
- var httpContent = response == null ? null : new StringContent(response);
- httpMessageHandlerMock.When(TestFcmEndpoint).Respond(req => new HttpResponseMessage { StatusCode = status, Content = httpContent });
- client.SetVapidDetails(TestSubject, TestPublicKey, TestPrivateKey);
- client.SendNotification(subscription, "123");
- }
+ httpMessageHandlerMock = new MockHttpMessageHandler();
+ client = new WebPushClient(httpMessageHandlerMock.ToHttpClient());
}
-}
\ No newline at end of file
+
+ [TestMethod]
+ public void TestSetTopic()
+ {
+ var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey);
+ var message = client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions { Topic = "testtopic" });
+ Assert.AreEqual(@"testtopic", message.Headers.GetValues(@"Topic").First());
+ }
+
+ [TestMethod]
+ public void TestSetTopicFailures()
+ {
+ var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey);
+ Assert.ThrowsExactly(() => client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions { Topic = "failing topic #3" }));
+ }
+
+
+ [TestMethod]
+ public void TestSetUrgency()
+ {
+ var subscription = new PushSubscription(TestGcmEndpoint, TestPublicKey, TestPrivateKey);
+ var message = client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions { Urgency = Urgency.VeryLow });
+ Assert.AreEqual(@"very-low", message.Headers.GetValues(@"Urgency").First());
+ }
+
+ [TestMethod]
+ public void TestSetContentEncoding()
+ {
+ 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());
+ }
+
+
+ [TestMethod]
+ public void TestSetVapidDetails()
+ {
+ client.SetVapidDetails(TestSubject, TestPublicKey, TestPrivateKey);
+
+ var subscription = new PushSubscription(TestFirefoxEndpoint, TestPublicKey, TestPrivateKey);
+ var message = client.GenerateRequestDetails(subscription, @"test payload", new WebPushOptions());
+ var authorizationHeader = message.Headers.GetValues(@"Authorization").First();
+ // var cryptoHeader = message.Headers.GetValues(@"Crypto-Key").First();
+
+ // Assert.StartsWith(@"WebPush ", authorizationHeader);
+ Assert.StartsWith(@"vapid ", authorizationHeader);
+ // Assert.Contains(@"p256ecdsa", cryptoHeader);
+ }
+
+ [TestMethod]
+ [DataRow(HttpStatusCode.Created)]
+ [DataRow(HttpStatusCode.Accepted)]
+ public void TestHandlingSuccessHttpCodes(HttpStatusCode status)
+ {
+ TestSendNotification(status);
+ }
+
+ [TestMethod]
+ [DataRow(HttpStatusCode.BadRequest, "Bad Request")]
+ [DataRow(HttpStatusCode.RequestEntityTooLarge, "Payload too large")]
+ [DataRow((HttpStatusCode)429, "Too many request")]
+ [DataRow(HttpStatusCode.NotFound, "Subscription no longer valid")]
+ [DataRow(HttpStatusCode.Gone, "Subscription no longer valid")]
+ [DataRow(HttpStatusCode.InternalServerError, "Received unexpected response code: 500")]
+ public void TestHandlingFailureHttpCodes(HttpStatusCode status, string expectedMessage)
+ {
+ var actual = Assert.ThrowsExactly(() => TestSendNotification(status));
+ Assert.AreEqual(expectedMessage, actual.Message);
+ }
+
+ [TestMethod]
+ [DataRow(HttpStatusCode.BadRequest, "authorization key missing", "Bad Request. Details: authorization key missing")]
+ [DataRow(HttpStatusCode.RequestEntityTooLarge, "max size is 512", "Payload too large. Details: max size is 512")]
+ [DataRow((HttpStatusCode)429, "the api is limited", "Too many request. Details: the api is limited")]
+ [DataRow(HttpStatusCode.NotFound, "", "Subscription no longer valid")]
+ [DataRow(HttpStatusCode.Gone, "", "Subscription no longer valid")]
+ [DataRow(HttpStatusCode.InternalServerError, "internal error", "Received unexpected response code: 500. Details: internal error")]
+ public void TestHandlingFailureMessages(HttpStatusCode status, string response, string expectedMessage)
+ {
+ var actual = Assert.ThrowsExactly(() => TestSendNotification(status, response));
+ Assert.AreEqual(expectedMessage, actual.Message);
+ }
+
+ [TestMethod]
+ [DataRow(1)]
+ [DataRow(5)]
+ [DataRow(10)]
+ [DataRow(50)]
+ public void TestHandleInvalidPublicKeys(int charactersToDrop)
+ {
+ var invalidKey = TestPublicKey.Substring(0, TestPublicKey.Length - charactersToDrop);
+ Assert.ThrowsExactly(() => TestSendNotification(HttpStatusCode.OK, response: null, invalidKey));
+ }
+
+ private void TestSendNotification(HttpStatusCode status, string response = null, string publicKey = TestPublicKey)
+ {
+ var subscription = new PushSubscription(TestFcmEndpoint, publicKey, TestPrivateKey);
+ var httpContent = response == null ? null : new StringContent(response);
+ httpMessageHandlerMock.When(TestFcmEndpoint).Respond(req => new HttpResponseMessage { StatusCode = status, Content = httpContent });
+ client.SetVapidDetails(TestSubject, TestPublicKey, TestPrivateKey);
+ client.SendNotification(subscription, "123", new WebPushOptions());
+ }
+
+}
diff --git a/WebPush.Test/packages.lock.json b/WebPush.Test/packages.lock.json
index e00192a..02cdb60 100644
--- a/WebPush.Test/packages.lock.json
+++ b/WebPush.Test/packages.lock.json
@@ -1,23 +1,15 @@
{
"version": 1,
"dependencies": {
- ".NETFramework,Version=v4.8": {
+ "net10.0": {
"Microsoft.NET.Test.Sdk": {
"type": "Direct",
- "requested": "[18.4.0, )",
- "resolved": "18.4.0",
- "contentHash": "w49iZdL4HL6V25l41NVQLXWQ+e71GvSkKVteMrOL02gP/PUkcnO/1yEb2s9FntU4wGmJWfKnyrRAhcMHd9ZZNA==",
- "dependencies": {
- "Microsoft.CodeCoverage": "18.4.0"
- }
- },
- "Microsoft.NETFramework.ReferenceAssemblies": {
- "type": "Direct",
- "requested": "[1.0.3, )",
- "resolved": "1.0.3",
- "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
+ "requested": "[18.5.1, )",
+ "resolved": "18.5.1",
+ "contentHash": "SfqVaLiIqAbRWuPg5BP4QFwBIirQj/YIL8Dhxl6zntBKbXp0cQykoV480SmwG+yRMiWptxEI6NbHQuGSZ8b97w==",
"dependencies": {
- "Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3"
+ "Microsoft.CodeCoverage": "18.5.1",
+ "Microsoft.TestPlatform.TestHost": "18.5.1"
}
},
"Moq": {
@@ -26,29 +18,27 @@
"resolved": "4.20.72",
"contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==",
"dependencies": {
- "Castle.Core": "5.1.1",
- "System.Threading.Tasks.Extensions": "4.5.4"
+ "Castle.Core": "5.1.1"
}
},
"MSTest.TestAdapter": {
"type": "Direct",
- "requested": "[4.2.1, )",
- "resolved": "4.2.1",
- "contentHash": "lZRgNzaQnffK4XLjM/og4Eoqp/3IkpcyJQQcyKXkPdkzCT3+ghpwHa9zG1xYhQDbUFoc54M+/waLwh31K9stDQ==",
+ "requested": "[4.2.2, )",
+ "resolved": "4.2.2",
+ "contentHash": "gMKNPoBnnlYM1DY+zAxJP05LDgXNHkjqxj6QQsm/O71nZh5BJ2SzsaTaQBQhXlu/HjzQ2CCbnMgufU13kYIpVA==",
"dependencies": {
- "MSTest.TestFramework": "4.2.1",
- "Microsoft.Testing.Extensions.VSTestBridge": "2.2.1",
- "Microsoft.Testing.Platform.MSBuild": "2.2.1",
- "System.Threading.Tasks.Extensions": "4.5.4"
+ "MSTest.TestFramework": "4.2.2",
+ "Microsoft.Testing.Extensions.VSTestBridge": "2.2.2",
+ "Microsoft.Testing.Platform.MSBuild": "2.2.2"
}
},
"MSTest.TestFramework": {
"type": "Direct",
- "requested": "[4.2.1, )",
- "resolved": "4.2.1",
- "contentHash": "I4/RbS2TpGZ56CE98+jPbrGlcerYtw2LvPVKzQGvyQQcJDekPy2Kd+fnThXYn+geJ1sW+vA9B7++rFNxvKcWxA==",
+ "requested": "[4.2.2, )",
+ "resolved": "4.2.2",
+ "contentHash": "IGjOt2kE6NxIgWYcM40DYSzCFaajLe6wHEICPRBnCqj1K4f9HrBLMPo4PE4mM/uKHNgDBvhvj/t1bXenUcQKqQ==",
"dependencies": {
- "MSTest.Analyzers": "4.2.1"
+ "MSTest.Analyzers": "4.2.2"
}
},
"RichardSzalay.MockHttp": {
@@ -60,336 +50,129 @@
"Castle.Core": {
"type": "Transitive",
"resolved": "5.1.1",
- "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g=="
- },
- "Microsoft.ApplicationInsights": {
- "type": "Transitive",
- "resolved": "2.23.0",
- "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==",
+ "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==",
"dependencies": {
- "System.Diagnostics.DiagnosticSource": "5.0.0"
+ "System.Diagnostics.EventLog": "6.0.0"
}
},
- "Microsoft.Bcl.AsyncInterfaces": {
+ "Microsoft.ApplicationInsights": {
"type": "Transitive",
- "resolved": "10.0.7",
- "contentHash": "g0Xp9A+B8jCf5pNIIhFOQXPJkte3D87shfTLY+ylwfSh22U5oQH6tvvmcUuqJvt/wtwKk0WdNp2OGEczHJlJdg==",
- "dependencies": {
- "System.Threading.Tasks.Extensions": "4.6.3"
- }
+ "resolved": "2.23.0",
+ "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw=="
},
"Microsoft.CodeCoverage": {
"type": "Transitive",
- "resolved": "18.4.0",
- "contentHash": "9O0BtCfzCWrkAmK187ugKdq72HHOXoOUjuWFDVc2LsZZ0pOnA9bTt+Sg9q4cF+MoAaUU+MuWtvBuFsnduviJow=="
+ "resolved": "18.5.1",
+ "contentHash": "vMFDR1ZjqzzgKmM0zrPie7Gv9Y+ZppjODB5Quzu9Eq0TlIusUfUCYFPEawO91zQuqwzvdFbJSU7WHNtjStffJQ=="
},
- "Microsoft.NETFramework.ReferenceAssemblies.net48": {
- "type": "Transitive",
- "resolved": "1.0.3",
- "contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ=="
- },
- "Microsoft.Testing.Extensions.Telemetry": {
- "type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "7zB8BjffOyvqfHF26rFVPuK0w1fCf5+j1tLuhHIr76CqxXkGb+fMJtq6YNOV+m6qPytExHMXxluk3RgJ+dSIqw==",
- "dependencies": {
- "Microsoft.ApplicationInsights": "2.23.0",
- "Microsoft.Testing.Platform": "2.2.1",
- "System.Diagnostics.DiagnosticSource": "6.0.0"
- }
- },
- "Microsoft.Testing.Extensions.TrxReport.Abstractions": {
- "type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "RD6D1Jx6cKDA5IHd1H2q8ylIuQG3PD+gdULI0JC8CvsRtaypFzTFpB5xDPuQi8o6kAkcM04cBhAiJPxZboNH2Q==",
- "dependencies": {
- "Microsoft.Testing.Platform": "2.2.1"
- }
- },
- "Microsoft.Testing.Extensions.VSTestBridge": {
- "type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "D8AGlkNtlTQPe3zf4SLnHBMr13lerMe0RuHSoRfnRatcuX/T7YbRtgn39rWBjKhXsNio0WXKrPKv3gfWE2I46w==",
- "dependencies": {
- "Microsoft.TestPlatform.ObjectModel": "18.3.0",
- "Microsoft.Testing.Extensions.Telemetry": "2.2.1",
- "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.1",
- "Microsoft.Testing.Platform": "2.2.1"
- }
- },
- "Microsoft.Testing.Platform": {
- "type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "9bbPuls/b6/vUFzxbSjJLZlJHyKBfOZE5kjIY+ITI2ASqlFPJhR83BdLydJeQOCLEZhEbrEcz5xtt1B69nwSVg=="
- },
- "Microsoft.Testing.Platform.MSBuild": {
- "type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "CSJOcZHfKlTyPbS0CTJk6iEnU4gJC+eUA5z72UBnMDRdgVHYOmB8k9Y7jT233gZjnCOQiYFg3acQHRfu2H62nw==",
- "dependencies": {
- "Microsoft.Testing.Platform": "2.2.1"
- }
- },
- "Microsoft.TestPlatform.ObjectModel": {
- "type": "Transitive",
- "resolved": "18.3.0",
- "contentHash": "AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ==",
- "dependencies": {
- "System.Reflection.Metadata": "8.0.0"
- }
- },
- "MSTest.Analyzers": {
- "type": "Transitive",
- "resolved": "4.2.1",
- "contentHash": "1i9jgE/42KGGyZ4s0MdrYM/Uu/dRYhbRfYQifcO0AZ6vw4sBXRjoQGQRGNSm771AYgPAmoGl0u4sJc2lMET6HQ=="
- },
- "Portable.BouncyCastle": {
- "type": "Transitive",
- "resolved": "1.9.0",
- "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw=="
- },
- "System.Buffers": {
- "type": "Transitive",
- "resolved": "4.6.1",
- "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw=="
- },
- "System.Collections.Immutable": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
- "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==",
- "dependencies": {
- "System.Memory": "4.5.5",
- "System.Runtime.CompilerServices.Unsafe": "6.0.0"
- }
- },
- "System.Diagnostics.DiagnosticSource": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "frQDfv0rl209cKm1lnwTgFPzNigy2EKk1BS3uAvHvlBVKe5cymGyHO+Sj+NLv5VF/AhHsqPIUUwya5oV4CHMUw==",
- "dependencies": {
- "System.Memory": "4.5.4",
- "System.Runtime.CompilerServices.Unsafe": "6.0.0"
- }
- },
- "System.IO.Pipelines": {
- "type": "Transitive",
- "resolved": "10.0.7",
- "contentHash": "LTxXYYKmRhPKWveYmfzuRTUnzsfY7CN+WOq6aTRgYE9vJ8BUvIWPCaSx4HxqBwXViTPSjR9cHDOVuVPuZGRR/Q==",
- "dependencies": {
- "System.Buffers": "4.6.1",
- "System.Memory": "4.6.3",
- "System.Threading.Tasks.Extensions": "4.6.3"
- }
+ "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg=="
},
- "System.Memory": {
- "type": "Transitive",
- "resolved": "4.6.3",
- "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==",
- "dependencies": {
- "System.Buffers": "4.6.1",
- "System.Numerics.Vectors": "4.6.1",
- "System.Runtime.CompilerServices.Unsafe": "6.1.2"
- }
- },
- "System.Numerics.Vectors": {
- "type": "Transitive",
- "resolved": "4.6.1",
- "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q=="
- },
- "System.Reflection.Metadata": {
+ "Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "8.0.0",
- "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==",
- "dependencies": {
- "System.Collections.Immutable": "8.0.0",
- "System.Memory": "4.5.5"
- }
- },
- "System.Runtime.CompilerServices.Unsafe": {
- "type": "Transitive",
- "resolved": "6.1.2",
- "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw=="
- },
- "System.Text.Encodings.Web": {
- "type": "Transitive",
- "resolved": "10.0.7",
- "contentHash": "WUH+viO8VDG8NpFKvOBwpeyKUiPOMz3kQpA6AKCD4b2NG1pBhyC4AwTb357iZmTxZDnkM4IsFnvzN8W8OKmsHg==",
+ "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
"dependencies": {
- "System.Buffers": "4.6.1",
- "System.Memory": "4.6.3",
- "System.Runtime.CompilerServices.Unsafe": "6.1.2"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
},
- "System.Text.Json": {
+ "Microsoft.IdentityModel.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.7",
- "contentHash": "F8Pu2QLUMeniVbtiyk7n7LCfFYxlcJ8ASaSwglJyq6dxa34iCQrikQszsgJClIJWuSWjcyhKkV7daAzYJqeVwA==",
- "dependencies": {
- "Microsoft.Bcl.AsyncInterfaces": "10.0.7",
- "System.Buffers": "4.6.1",
- "System.IO.Pipelines": "10.0.7",
- "System.Memory": "4.6.3",
- "System.Runtime.CompilerServices.Unsafe": "6.1.2",
- "System.Text.Encodings.Web": "10.0.7",
- "System.Threading.Tasks.Extensions": "4.6.3",
- "System.ValueTuple": "4.6.2"
- }
+ "resolved": "8.18.0",
+ "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ=="
},
- "System.Threading.Tasks.Extensions": {
+ "Microsoft.IdentityModel.JsonWebTokens": {
"type": "Transitive",
- "resolved": "4.6.3",
- "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==",
+ "resolved": "8.18.0",
+ "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==",
"dependencies": {
- "System.Runtime.CompilerServices.Unsafe": "6.1.2"
+ "Microsoft.IdentityModel.Tokens": "8.18.0"
}
},
- "System.ValueTuple": {
+ "Microsoft.IdentityModel.Logging": {
"type": "Transitive",
- "resolved": "4.6.2",
- "contentHash": "yQgmjfFximrNm9LIV3mL6T5MzjeC+epeE5rl4hXxAlYmxby7RM1dPSkIKXk9HNkl6G54h2JHOmLD46+Pey+IRg=="
- },
- "webpush": {
- "type": "Project",
- "dependencies": {
- "Portable.BouncyCastle": "[1.9.0, )",
- "System.Text.Json": "[10.0.7, )"
- }
- }
- },
- "net10.0": {
- "Microsoft.NET.Test.Sdk": {
- "type": "Direct",
- "requested": "[18.4.0, )",
- "resolved": "18.4.0",
- "contentHash": "w49iZdL4HL6V25l41NVQLXWQ+e71GvSkKVteMrOL02gP/PUkcnO/1yEb2s9FntU4wGmJWfKnyrRAhcMHd9ZZNA==",
- "dependencies": {
- "Microsoft.CodeCoverage": "18.4.0",
- "Microsoft.TestPlatform.TestHost": "18.4.0"
- }
- },
- "Moq": {
- "type": "Direct",
- "requested": "[4.20.72, )",
- "resolved": "4.20.72",
- "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==",
+ "resolved": "8.18.0",
+ "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==",
"dependencies": {
- "Castle.Core": "5.1.1"
+ "Microsoft.IdentityModel.Abstractions": "8.18.0"
}
},
- "MSTest.TestAdapter": {
- "type": "Direct",
- "requested": "[4.2.1, )",
- "resolved": "4.2.1",
- "contentHash": "lZRgNzaQnffK4XLjM/og4Eoqp/3IkpcyJQQcyKXkPdkzCT3+ghpwHa9zG1xYhQDbUFoc54M+/waLwh31K9stDQ==",
- "dependencies": {
- "MSTest.TestFramework": "4.2.1",
- "Microsoft.Testing.Extensions.VSTestBridge": "2.2.1",
- "Microsoft.Testing.Platform.MSBuild": "2.2.1"
- }
- },
- "MSTest.TestFramework": {
- "type": "Direct",
- "requested": "[4.2.1, )",
- "resolved": "4.2.1",
- "contentHash": "I4/RbS2TpGZ56CE98+jPbrGlcerYtw2LvPVKzQGvyQQcJDekPy2Kd+fnThXYn+geJ1sW+vA9B7++rFNxvKcWxA==",
- "dependencies": {
- "MSTest.Analyzers": "4.2.1"
- }
- },
- "RichardSzalay.MockHttp": {
- "type": "Direct",
- "requested": "[7.0.0, )",
- "resolved": "7.0.0",
- "contentHash": "QwnauYiaywp65QKFnP+wvgiQ2D8Pv888qB2dyfd7MSVDF06sIvxqASenk+RxsWybyyt+Hu1Y251wQxpHTv3UYg=="
- },
- "Castle.Core": {
+ "Microsoft.IdentityModel.Tokens": {
"type": "Transitive",
- "resolved": "5.1.1",
- "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==",
+ "resolved": "8.18.0",
+ "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==",
"dependencies": {
- "System.Diagnostics.EventLog": "6.0.0"
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.0",
+ "Microsoft.IdentityModel.Logging": "8.18.0"
}
},
- "Microsoft.ApplicationInsights": {
- "type": "Transitive",
- "resolved": "2.23.0",
- "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw=="
- },
- "Microsoft.CodeCoverage": {
- "type": "Transitive",
- "resolved": "18.4.0",
- "contentHash": "9O0BtCfzCWrkAmK187ugKdq72HHOXoOUjuWFDVc2LsZZ0pOnA9bTt+Sg9q4cF+MoAaUU+MuWtvBuFsnduviJow=="
- },
"Microsoft.Testing.Extensions.Telemetry": {
"type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "7zB8BjffOyvqfHF26rFVPuK0w1fCf5+j1tLuhHIr76CqxXkGb+fMJtq6YNOV+m6qPytExHMXxluk3RgJ+dSIqw==",
+ "resolved": "2.2.2",
+ "contentHash": "qKRghdaDiC88N1s3LDJO7zW74QNZu/ErnTxuG7R9u9UORn6pTwdqbi7X+eY4UQb+7YV2gR2yz8eRelvOWQVxhA==",
"dependencies": {
"Microsoft.ApplicationInsights": "2.23.0",
- "Microsoft.Testing.Platform": "2.2.1"
+ "Microsoft.Testing.Platform": "2.2.2"
}
},
"Microsoft.Testing.Extensions.TrxReport.Abstractions": {
"type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "RD6D1Jx6cKDA5IHd1H2q8ylIuQG3PD+gdULI0JC8CvsRtaypFzTFpB5xDPuQi8o6kAkcM04cBhAiJPxZboNH2Q==",
+ "resolved": "2.2.2",
+ "contentHash": "MuOC3Be70FPysaPxaO0f3GFoXU49UwnKCVDWfFrOZ93h955KZ6MKiJ6vwt/2r4e1wkLDoJFbkQzi/MNbpe4oXQ==",
"dependencies": {
- "Microsoft.Testing.Platform": "2.2.1"
+ "Microsoft.Testing.Platform": "2.2.2"
}
},
"Microsoft.Testing.Extensions.VSTestBridge": {
"type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "D8AGlkNtlTQPe3zf4SLnHBMr13lerMe0RuHSoRfnRatcuX/T7YbRtgn39rWBjKhXsNio0WXKrPKv3gfWE2I46w==",
+ "resolved": "2.2.2",
+ "contentHash": "dyo49lXzY3seyfEgv7qrkIqdvrMAjdJjmY0VDPE//UPK89c+65cqQm8m+FO5XbRpr8gB6AUi5KCRbEl1eRlwQA==",
"dependencies": {
"Microsoft.TestPlatform.ObjectModel": "18.3.0",
- "Microsoft.Testing.Extensions.Telemetry": "2.2.1",
- "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.1",
- "Microsoft.Testing.Platform": "2.2.1"
+ "Microsoft.Testing.Extensions.Telemetry": "2.2.2",
+ "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.2",
+ "Microsoft.Testing.Platform": "2.2.2"
}
},
"Microsoft.Testing.Platform": {
"type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "9bbPuls/b6/vUFzxbSjJLZlJHyKBfOZE5kjIY+ITI2ASqlFPJhR83BdLydJeQOCLEZhEbrEcz5xtt1B69nwSVg=="
+ "resolved": "2.2.2",
+ "contentHash": "9mUsTOri0aVqBX7/EJwqVJxVwdOzGUVJqK1H2EMfIl9xxJuSdqhfAlJbukl/iNugvi4+cmQs/LI8PLTDUT9P1A=="
},
"Microsoft.Testing.Platform.MSBuild": {
"type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "CSJOcZHfKlTyPbS0CTJk6iEnU4gJC+eUA5z72UBnMDRdgVHYOmB8k9Y7jT233gZjnCOQiYFg3acQHRfu2H62nw==",
+ "resolved": "2.2.2",
+ "contentHash": "acgkTLYA8C39oe5b5ISmydBshR0XO6v8z3/CXAsLmPQ3xAiomHuPoTAgY28tjQLcwPZOu4GX034BXWvmsVpzIg==",
"dependencies": {
- "Microsoft.Testing.Platform": "2.2.1"
+ "Microsoft.Testing.Platform": "2.2.2"
}
},
"Microsoft.TestPlatform.ObjectModel": {
"type": "Transitive",
- "resolved": "18.4.0",
- "contentHash": "4L6m2kS2pY5uJ9cpeRxzW22opr6ttScIRqsOpMDQpgENp/ZwxkkQCcmc6LRSURo2dFaaSW5KVflQZvroiJ7Wzg=="
+ "resolved": "18.5.1",
+ "contentHash": "KNZd+M0S0rz5eNAln0pbZX+A/RbokYZCbGKx4fN4CkhtWhkz6nSJDO+9LGYjRE4d0WPVriJ2JnVubkjt3+PpMg=="
},
"Microsoft.TestPlatform.TestHost": {
"type": "Transitive",
- "resolved": "18.4.0",
- "contentHash": "gZsCHI+zOmZCcKZieIL4Jg14qKD2OGZOmX5DehuIk1EA9BN6Crm0+taXQNEuajOH1G9CCyBxw8VWR4t5tumcng==",
+ "resolved": "18.5.1",
+ "contentHash": "RM+3JNHEoHOCFXzVntUcIiYxzPjzBN0N8wto6HYXi76YyBTZ/3CeRL8U+Pk5zx3AUrOmHxDvKJwGUCdElU9bJg==",
"dependencies": {
- "Microsoft.TestPlatform.ObjectModel": "18.4.0",
+ "Microsoft.TestPlatform.ObjectModel": "18.5.1",
"Newtonsoft.Json": "13.0.3"
}
},
"MSTest.Analyzers": {
"type": "Transitive",
- "resolved": "4.2.1",
- "contentHash": "1i9jgE/42KGGyZ4s0MdrYM/Uu/dRYhbRfYQifcO0AZ6vw4sBXRjoQGQRGNSm771AYgPAmoGl0u4sJc2lMET6HQ=="
+ "resolved": "4.2.2",
+ "contentHash": "0VUx09Q6MdPlTCG+xTqEoXIrjr32F1Ya5EI/hfQdRSczZh61AWWtCdGXRCe3DDfUUbPVvFBZTJcrlTT1Cv25Dg=="
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
- "Portable.BouncyCastle": {
- "type": "Transitive",
- "resolved": "1.9.0",
- "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw=="
- },
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "6.0.0",
@@ -398,19 +181,20 @@
"webpush": {
"type": "Project",
"dependencies": {
- "Portable.BouncyCastle": "[1.9.0, )"
+ "Microsoft.IdentityModel.JsonWebTokens": "[8.18.0, )",
+ "Microsoft.IdentityModel.Tokens": "[8.18.0, )"
}
}
},
"net8.0": {
"Microsoft.NET.Test.Sdk": {
"type": "Direct",
- "requested": "[18.4.0, )",
- "resolved": "18.4.0",
- "contentHash": "w49iZdL4HL6V25l41NVQLXWQ+e71GvSkKVteMrOL02gP/PUkcnO/1yEb2s9FntU4wGmJWfKnyrRAhcMHd9ZZNA==",
+ "requested": "[18.5.1, )",
+ "resolved": "18.5.1",
+ "contentHash": "SfqVaLiIqAbRWuPg5BP4QFwBIirQj/YIL8Dhxl6zntBKbXp0cQykoV480SmwG+yRMiWptxEI6NbHQuGSZ8b97w==",
"dependencies": {
- "Microsoft.CodeCoverage": "18.4.0",
- "Microsoft.TestPlatform.TestHost": "18.4.0"
+ "Microsoft.CodeCoverage": "18.5.1",
+ "Microsoft.TestPlatform.TestHost": "18.5.1"
}
},
"Moq": {
@@ -424,22 +208,22 @@
},
"MSTest.TestAdapter": {
"type": "Direct",
- "requested": "[4.2.1, )",
- "resolved": "4.2.1",
- "contentHash": "lZRgNzaQnffK4XLjM/og4Eoqp/3IkpcyJQQcyKXkPdkzCT3+ghpwHa9zG1xYhQDbUFoc54M+/waLwh31K9stDQ==",
+ "requested": "[4.2.2, )",
+ "resolved": "4.2.2",
+ "contentHash": "gMKNPoBnnlYM1DY+zAxJP05LDgXNHkjqxj6QQsm/O71nZh5BJ2SzsaTaQBQhXlu/HjzQ2CCbnMgufU13kYIpVA==",
"dependencies": {
- "MSTest.TestFramework": "4.2.1",
- "Microsoft.Testing.Extensions.VSTestBridge": "2.2.1",
- "Microsoft.Testing.Platform.MSBuild": "2.2.1"
+ "MSTest.TestFramework": "4.2.2",
+ "Microsoft.Testing.Extensions.VSTestBridge": "2.2.2",
+ "Microsoft.Testing.Platform.MSBuild": "2.2.2"
}
},
"MSTest.TestFramework": {
"type": "Direct",
- "requested": "[4.2.1, )",
- "resolved": "4.2.1",
- "contentHash": "I4/RbS2TpGZ56CE98+jPbrGlcerYtw2LvPVKzQGvyQQcJDekPy2Kd+fnThXYn+geJ1sW+vA9B7++rFNxvKcWxA==",
+ "requested": "[4.2.2, )",
+ "resolved": "4.2.2",
+ "contentHash": "IGjOt2kE6NxIgWYcM40DYSzCFaajLe6wHEICPRBnCqj1K4f9HrBLMPo4PE4mM/uKHNgDBvhvj/t1bXenUcQKqQ==",
"dependencies": {
- "MSTest.Analyzers": "4.2.1"
+ "MSTest.Analyzers": "4.2.2"
}
},
"RichardSzalay.MockHttp": {
@@ -463,79 +247,117 @@
},
"Microsoft.CodeCoverage": {
"type": "Transitive",
- "resolved": "18.4.0",
- "contentHash": "9O0BtCfzCWrkAmK187ugKdq72HHOXoOUjuWFDVc2LsZZ0pOnA9bTt+Sg9q4cF+MoAaUU+MuWtvBuFsnduviJow=="
+ "resolved": "18.5.1",
+ "contentHash": "vMFDR1ZjqzzgKmM0zrPie7Gv9Y+ZppjODB5Quzu9Eq0TlIusUfUCYFPEawO91zQuqwzvdFbJSU7WHNtjStffJQ=="
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg=="
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
+ }
+ },
+ "Microsoft.IdentityModel.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.18.0",
+ "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ=="
+ },
+ "Microsoft.IdentityModel.JsonWebTokens": {
+ "type": "Transitive",
+ "resolved": "8.18.0",
+ "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==",
+ "dependencies": {
+ "Microsoft.IdentityModel.Tokens": "8.18.0"
+ }
+ },
+ "Microsoft.IdentityModel.Logging": {
+ "type": "Transitive",
+ "resolved": "8.18.0",
+ "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==",
+ "dependencies": {
+ "Microsoft.IdentityModel.Abstractions": "8.18.0"
+ }
+ },
+ "Microsoft.IdentityModel.Tokens": {
+ "type": "Transitive",
+ "resolved": "8.18.0",
+ "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.0",
+ "Microsoft.IdentityModel.Logging": "8.18.0"
+ }
},
"Microsoft.Testing.Extensions.Telemetry": {
"type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "7zB8BjffOyvqfHF26rFVPuK0w1fCf5+j1tLuhHIr76CqxXkGb+fMJtq6YNOV+m6qPytExHMXxluk3RgJ+dSIqw==",
+ "resolved": "2.2.2",
+ "contentHash": "qKRghdaDiC88N1s3LDJO7zW74QNZu/ErnTxuG7R9u9UORn6pTwdqbi7X+eY4UQb+7YV2gR2yz8eRelvOWQVxhA==",
"dependencies": {
"Microsoft.ApplicationInsights": "2.23.0",
- "Microsoft.Testing.Platform": "2.2.1"
+ "Microsoft.Testing.Platform": "2.2.2"
}
},
"Microsoft.Testing.Extensions.TrxReport.Abstractions": {
"type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "RD6D1Jx6cKDA5IHd1H2q8ylIuQG3PD+gdULI0JC8CvsRtaypFzTFpB5xDPuQi8o6kAkcM04cBhAiJPxZboNH2Q==",
+ "resolved": "2.2.2",
+ "contentHash": "MuOC3Be70FPysaPxaO0f3GFoXU49UwnKCVDWfFrOZ93h955KZ6MKiJ6vwt/2r4e1wkLDoJFbkQzi/MNbpe4oXQ==",
"dependencies": {
- "Microsoft.Testing.Platform": "2.2.1"
+ "Microsoft.Testing.Platform": "2.2.2"
}
},
"Microsoft.Testing.Extensions.VSTestBridge": {
"type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "D8AGlkNtlTQPe3zf4SLnHBMr13lerMe0RuHSoRfnRatcuX/T7YbRtgn39rWBjKhXsNio0WXKrPKv3gfWE2I46w==",
+ "resolved": "2.2.2",
+ "contentHash": "dyo49lXzY3seyfEgv7qrkIqdvrMAjdJjmY0VDPE//UPK89c+65cqQm8m+FO5XbRpr8gB6AUi5KCRbEl1eRlwQA==",
"dependencies": {
"Microsoft.TestPlatform.ObjectModel": "18.3.0",
- "Microsoft.Testing.Extensions.Telemetry": "2.2.1",
- "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.1",
- "Microsoft.Testing.Platform": "2.2.1"
+ "Microsoft.Testing.Extensions.Telemetry": "2.2.2",
+ "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.2",
+ "Microsoft.Testing.Platform": "2.2.2"
}
},
"Microsoft.Testing.Platform": {
"type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "9bbPuls/b6/vUFzxbSjJLZlJHyKBfOZE5kjIY+ITI2ASqlFPJhR83BdLydJeQOCLEZhEbrEcz5xtt1B69nwSVg=="
+ "resolved": "2.2.2",
+ "contentHash": "9mUsTOri0aVqBX7/EJwqVJxVwdOzGUVJqK1H2EMfIl9xxJuSdqhfAlJbukl/iNugvi4+cmQs/LI8PLTDUT9P1A=="
},
"Microsoft.Testing.Platform.MSBuild": {
"type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "CSJOcZHfKlTyPbS0CTJk6iEnU4gJC+eUA5z72UBnMDRdgVHYOmB8k9Y7jT233gZjnCOQiYFg3acQHRfu2H62nw==",
+ "resolved": "2.2.2",
+ "contentHash": "acgkTLYA8C39oe5b5ISmydBshR0XO6v8z3/CXAsLmPQ3xAiomHuPoTAgY28tjQLcwPZOu4GX034BXWvmsVpzIg==",
"dependencies": {
- "Microsoft.Testing.Platform": "2.2.1"
+ "Microsoft.Testing.Platform": "2.2.2"
}
},
"Microsoft.TestPlatform.ObjectModel": {
"type": "Transitive",
- "resolved": "18.4.0",
- "contentHash": "4L6m2kS2pY5uJ9cpeRxzW22opr6ttScIRqsOpMDQpgENp/ZwxkkQCcmc6LRSURo2dFaaSW5KVflQZvroiJ7Wzg=="
+ "resolved": "18.5.1",
+ "contentHash": "KNZd+M0S0rz5eNAln0pbZX+A/RbokYZCbGKx4fN4CkhtWhkz6nSJDO+9LGYjRE4d0WPVriJ2JnVubkjt3+PpMg=="
},
"Microsoft.TestPlatform.TestHost": {
"type": "Transitive",
- "resolved": "18.4.0",
- "contentHash": "gZsCHI+zOmZCcKZieIL4Jg14qKD2OGZOmX5DehuIk1EA9BN6Crm0+taXQNEuajOH1G9CCyBxw8VWR4t5tumcng==",
+ "resolved": "18.5.1",
+ "contentHash": "RM+3JNHEoHOCFXzVntUcIiYxzPjzBN0N8wto6HYXi76YyBTZ/3CeRL8U+Pk5zx3AUrOmHxDvKJwGUCdElU9bJg==",
"dependencies": {
- "Microsoft.TestPlatform.ObjectModel": "18.4.0",
+ "Microsoft.TestPlatform.ObjectModel": "18.5.1",
"Newtonsoft.Json": "13.0.3"
}
},
"MSTest.Analyzers": {
"type": "Transitive",
- "resolved": "4.2.1",
- "contentHash": "1i9jgE/42KGGyZ4s0MdrYM/Uu/dRYhbRfYQifcO0AZ6vw4sBXRjoQGQRGNSm771AYgPAmoGl0u4sJc2lMET6HQ=="
+ "resolved": "4.2.2",
+ "contentHash": "0VUx09Q6MdPlTCG+xTqEoXIrjr32F1Ya5EI/hfQdRSczZh61AWWtCdGXRCe3DDfUUbPVvFBZTJcrlTT1Cv25Dg=="
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
- "Portable.BouncyCastle": {
- "type": "Transitive",
- "resolved": "1.9.0",
- "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw=="
- },
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "6.0.0",
@@ -544,19 +366,20 @@
"webpush": {
"type": "Project",
"dependencies": {
- "Portable.BouncyCastle": "[1.9.0, )"
+ "Microsoft.IdentityModel.JsonWebTokens": "[8.18.0, )",
+ "Microsoft.IdentityModel.Tokens": "[8.18.0, )"
}
}
},
"net9.0": {
"Microsoft.NET.Test.Sdk": {
"type": "Direct",
- "requested": "[18.4.0, )",
- "resolved": "18.4.0",
- "contentHash": "w49iZdL4HL6V25l41NVQLXWQ+e71GvSkKVteMrOL02gP/PUkcnO/1yEb2s9FntU4wGmJWfKnyrRAhcMHd9ZZNA==",
+ "requested": "[18.5.1, )",
+ "resolved": "18.5.1",
+ "contentHash": "SfqVaLiIqAbRWuPg5BP4QFwBIirQj/YIL8Dhxl6zntBKbXp0cQykoV480SmwG+yRMiWptxEI6NbHQuGSZ8b97w==",
"dependencies": {
- "Microsoft.CodeCoverage": "18.4.0",
- "Microsoft.TestPlatform.TestHost": "18.4.0"
+ "Microsoft.CodeCoverage": "18.5.1",
+ "Microsoft.TestPlatform.TestHost": "18.5.1"
}
},
"Moq": {
@@ -570,22 +393,22 @@
},
"MSTest.TestAdapter": {
"type": "Direct",
- "requested": "[4.2.1, )",
- "resolved": "4.2.1",
- "contentHash": "lZRgNzaQnffK4XLjM/og4Eoqp/3IkpcyJQQcyKXkPdkzCT3+ghpwHa9zG1xYhQDbUFoc54M+/waLwh31K9stDQ==",
+ "requested": "[4.2.2, )",
+ "resolved": "4.2.2",
+ "contentHash": "gMKNPoBnnlYM1DY+zAxJP05LDgXNHkjqxj6QQsm/O71nZh5BJ2SzsaTaQBQhXlu/HjzQ2CCbnMgufU13kYIpVA==",
"dependencies": {
- "MSTest.TestFramework": "4.2.1",
- "Microsoft.Testing.Extensions.VSTestBridge": "2.2.1",
- "Microsoft.Testing.Platform.MSBuild": "2.2.1"
+ "MSTest.TestFramework": "4.2.2",
+ "Microsoft.Testing.Extensions.VSTestBridge": "2.2.2",
+ "Microsoft.Testing.Platform.MSBuild": "2.2.2"
}
},
"MSTest.TestFramework": {
"type": "Direct",
- "requested": "[4.2.1, )",
- "resolved": "4.2.1",
- "contentHash": "I4/RbS2TpGZ56CE98+jPbrGlcerYtw2LvPVKzQGvyQQcJDekPy2Kd+fnThXYn+geJ1sW+vA9B7++rFNxvKcWxA==",
+ "requested": "[4.2.2, )",
+ "resolved": "4.2.2",
+ "contentHash": "IGjOt2kE6NxIgWYcM40DYSzCFaajLe6wHEICPRBnCqj1K4f9HrBLMPo4PE4mM/uKHNgDBvhvj/t1bXenUcQKqQ==",
"dependencies": {
- "MSTest.Analyzers": "4.2.1"
+ "MSTest.Analyzers": "4.2.2"
}
},
"RichardSzalay.MockHttp": {
@@ -609,79 +432,117 @@
},
"Microsoft.CodeCoverage": {
"type": "Transitive",
- "resolved": "18.4.0",
- "contentHash": "9O0BtCfzCWrkAmK187ugKdq72HHOXoOUjuWFDVc2LsZZ0pOnA9bTt+Sg9q4cF+MoAaUU+MuWtvBuFsnduviJow=="
+ "resolved": "18.5.1",
+ "contentHash": "vMFDR1ZjqzzgKmM0zrPie7Gv9Y+ZppjODB5Quzu9Eq0TlIusUfUCYFPEawO91zQuqwzvdFbJSU7WHNtjStffJQ=="
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg=="
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
+ }
+ },
+ "Microsoft.IdentityModel.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.18.0",
+ "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ=="
+ },
+ "Microsoft.IdentityModel.JsonWebTokens": {
+ "type": "Transitive",
+ "resolved": "8.18.0",
+ "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==",
+ "dependencies": {
+ "Microsoft.IdentityModel.Tokens": "8.18.0"
+ }
+ },
+ "Microsoft.IdentityModel.Logging": {
+ "type": "Transitive",
+ "resolved": "8.18.0",
+ "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==",
+ "dependencies": {
+ "Microsoft.IdentityModel.Abstractions": "8.18.0"
+ }
+ },
+ "Microsoft.IdentityModel.Tokens": {
+ "type": "Transitive",
+ "resolved": "8.18.0",
+ "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.0",
+ "Microsoft.IdentityModel.Logging": "8.18.0"
+ }
},
"Microsoft.Testing.Extensions.Telemetry": {
"type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "7zB8BjffOyvqfHF26rFVPuK0w1fCf5+j1tLuhHIr76CqxXkGb+fMJtq6YNOV+m6qPytExHMXxluk3RgJ+dSIqw==",
+ "resolved": "2.2.2",
+ "contentHash": "qKRghdaDiC88N1s3LDJO7zW74QNZu/ErnTxuG7R9u9UORn6pTwdqbi7X+eY4UQb+7YV2gR2yz8eRelvOWQVxhA==",
"dependencies": {
"Microsoft.ApplicationInsights": "2.23.0",
- "Microsoft.Testing.Platform": "2.2.1"
+ "Microsoft.Testing.Platform": "2.2.2"
}
},
"Microsoft.Testing.Extensions.TrxReport.Abstractions": {
"type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "RD6D1Jx6cKDA5IHd1H2q8ylIuQG3PD+gdULI0JC8CvsRtaypFzTFpB5xDPuQi8o6kAkcM04cBhAiJPxZboNH2Q==",
+ "resolved": "2.2.2",
+ "contentHash": "MuOC3Be70FPysaPxaO0f3GFoXU49UwnKCVDWfFrOZ93h955KZ6MKiJ6vwt/2r4e1wkLDoJFbkQzi/MNbpe4oXQ==",
"dependencies": {
- "Microsoft.Testing.Platform": "2.2.1"
+ "Microsoft.Testing.Platform": "2.2.2"
}
},
"Microsoft.Testing.Extensions.VSTestBridge": {
"type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "D8AGlkNtlTQPe3zf4SLnHBMr13lerMe0RuHSoRfnRatcuX/T7YbRtgn39rWBjKhXsNio0WXKrPKv3gfWE2I46w==",
+ "resolved": "2.2.2",
+ "contentHash": "dyo49lXzY3seyfEgv7qrkIqdvrMAjdJjmY0VDPE//UPK89c+65cqQm8m+FO5XbRpr8gB6AUi5KCRbEl1eRlwQA==",
"dependencies": {
"Microsoft.TestPlatform.ObjectModel": "18.3.0",
- "Microsoft.Testing.Extensions.Telemetry": "2.2.1",
- "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.1",
- "Microsoft.Testing.Platform": "2.2.1"
+ "Microsoft.Testing.Extensions.Telemetry": "2.2.2",
+ "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.2.2",
+ "Microsoft.Testing.Platform": "2.2.2"
}
},
"Microsoft.Testing.Platform": {
"type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "9bbPuls/b6/vUFzxbSjJLZlJHyKBfOZE5kjIY+ITI2ASqlFPJhR83BdLydJeQOCLEZhEbrEcz5xtt1B69nwSVg=="
+ "resolved": "2.2.2",
+ "contentHash": "9mUsTOri0aVqBX7/EJwqVJxVwdOzGUVJqK1H2EMfIl9xxJuSdqhfAlJbukl/iNugvi4+cmQs/LI8PLTDUT9P1A=="
},
"Microsoft.Testing.Platform.MSBuild": {
"type": "Transitive",
- "resolved": "2.2.1",
- "contentHash": "CSJOcZHfKlTyPbS0CTJk6iEnU4gJC+eUA5z72UBnMDRdgVHYOmB8k9Y7jT233gZjnCOQiYFg3acQHRfu2H62nw==",
+ "resolved": "2.2.2",
+ "contentHash": "acgkTLYA8C39oe5b5ISmydBshR0XO6v8z3/CXAsLmPQ3xAiomHuPoTAgY28tjQLcwPZOu4GX034BXWvmsVpzIg==",
"dependencies": {
- "Microsoft.Testing.Platform": "2.2.1"
+ "Microsoft.Testing.Platform": "2.2.2"
}
},
"Microsoft.TestPlatform.ObjectModel": {
"type": "Transitive",
- "resolved": "18.4.0",
- "contentHash": "4L6m2kS2pY5uJ9cpeRxzW22opr6ttScIRqsOpMDQpgENp/ZwxkkQCcmc6LRSURo2dFaaSW5KVflQZvroiJ7Wzg=="
+ "resolved": "18.5.1",
+ "contentHash": "KNZd+M0S0rz5eNAln0pbZX+A/RbokYZCbGKx4fN4CkhtWhkz6nSJDO+9LGYjRE4d0WPVriJ2JnVubkjt3+PpMg=="
},
"Microsoft.TestPlatform.TestHost": {
"type": "Transitive",
- "resolved": "18.4.0",
- "contentHash": "gZsCHI+zOmZCcKZieIL4Jg14qKD2OGZOmX5DehuIk1EA9BN6Crm0+taXQNEuajOH1G9CCyBxw8VWR4t5tumcng==",
+ "resolved": "18.5.1",
+ "contentHash": "RM+3JNHEoHOCFXzVntUcIiYxzPjzBN0N8wto6HYXi76YyBTZ/3CeRL8U+Pk5zx3AUrOmHxDvKJwGUCdElU9bJg==",
"dependencies": {
- "Microsoft.TestPlatform.ObjectModel": "18.4.0",
+ "Microsoft.TestPlatform.ObjectModel": "18.5.1",
"Newtonsoft.Json": "13.0.3"
}
},
"MSTest.Analyzers": {
"type": "Transitive",
- "resolved": "4.2.1",
- "contentHash": "1i9jgE/42KGGyZ4s0MdrYM/Uu/dRYhbRfYQifcO0AZ6vw4sBXRjoQGQRGNSm771AYgPAmoGl0u4sJc2lMET6HQ=="
+ "resolved": "4.2.2",
+ "contentHash": "0VUx09Q6MdPlTCG+xTqEoXIrjr32F1Ya5EI/hfQdRSczZh61AWWtCdGXRCe3DDfUUbPVvFBZTJcrlTT1Cv25Dg=="
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
- "Portable.BouncyCastle": {
- "type": "Transitive",
- "resolved": "1.9.0",
- "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw=="
- },
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "6.0.0",
@@ -690,7 +551,8 @@
"webpush": {
"type": "Project",
"dependencies": {
- "Portable.BouncyCastle": "[1.9.0, )"
+ "Microsoft.IdentityModel.JsonWebTokens": "[8.18.0, )",
+ "Microsoft.IdentityModel.Tokens": "[8.18.0, )"
}
}
}
diff --git a/WebPush/IWebPushClient.cs b/WebPush/IWebPushClient.cs
index 005be35..716d517 100644
--- a/WebPush/IWebPushClient.cs
+++ b/WebPush/IWebPushClient.cs
@@ -4,114 +4,127 @@
using System.Threading;
using System.Threading.Tasks;
-namespace WebPush
+namespace WebPush;
+
+public interface IWebPushClient : IDisposable
{
- public interface IWebPushClient : IDisposable
- {
- ///
- /// When sending messages to a GCM endpoint you need to set the GCM API key
- /// by either calling setGcmApiKey() or passing in the API key as an option
- /// to sendNotification()
- ///
- /// The API key to send with the GCM request.
- void SetGcmApiKey(string gcmApiKey);
+ ///
+ /// When marking requests where you want to define VAPID details, call this method
+ /// before sendNotifications() or pass in the details and options to
+ /// sendNotification.
+ ///
+ ///
+ void SetVapidDetails(VapidDetails vapidDetails);
- ///
- /// When marking requests where you want to define VAPID details, call this method
- /// before sendNotifications() or pass in the details and options to
- /// sendNotification.
- ///
- ///
- void SetVapidDetails(VapidDetails vapidDetails);
+ ///
+ /// When marking requests where you want to define VAPID details, call this method
+ /// before sendNotifications() or pass in the details and options to
+ /// sendNotification.
+ ///
+ /// This must be either a URL or a 'mailto:' address
+ /// The public VAPID key as a base64 encoded string
+ /// The private VAPID key as a base64 encoded string
+ void SetVapidDetails(string subject, string publicKey, string privateKey);
- ///
- /// When marking requests where you want to define VAPID details, call this method
- /// before sendNotifications() or pass in the details and options to
- /// sendNotification.
- ///
- /// This must be either a URL or a 'mailto:' address
- /// The public VAPID key as a base64 encoded string
- /// The private VAPID key as a base64 encoded string
- void SetVapidDetails(string subject, string publicKey, string privateKey);
+ ///
+ /// To get a request without sending a push notification call this method.
+ /// This method will throw an ArgumentException if there is an issue with the input.
+ ///
+ /// The PushSubscription you wish to send the notification to.
+ /// The payload you wish to send to the user
+ ///
+ /// Options for vapid keys can be passed in if they are unique for each
+ /// notification.
+ ///
+ /// A HttpRequestMessage object that can be sent.
+ [Obsolete("Use GenerateRequestDetails with WebPushOptions")]
+ HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string? payload,
+ Dictionary? options = null);
- ///
- /// To get a request without sending a push notification call this method.
- /// This method will throw an ArgumentException if there is an issue with the input.
- ///
- /// The PushSubscription you wish to send the notification to.
- /// The payload you wish to send to the user
- ///
- /// Options for the GCM API key and vapid keys can be passed in if they are unique for each
- /// notification.
- ///
- /// A HttpRequestMessage object that can be sent.
- HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string payload,
- Dictionary options = null);
+ ///
+ /// To get a request without sending a push notification call this method.
+ /// This method will throw an ArgumentException if there is an issue with the input.
+ ///
+ /// The PushSubscription you wish to send the notification to.
+ /// The payload you wish to send to the user
+ ///
+ /// Options for the web push request, including vapid keys, if they are unique for each
+ /// notification.
+ ///
+ /// A HttpRequestMessage object that can be sent.
+ public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string? payload, WebPushOptions? options = null);
- ///
- /// To send a push notification call this method with a subscription, optional payload and any options
- /// Will exception if unsuccessful
- ///
- /// The PushSubscription you wish to send the notification to.
- /// The payload you wish to send to the user
- ///
- /// Options for the GCM API key and vapid keys can be passed in if they are unique for each
- /// notification.
- ///
- void SendNotification(PushSubscription subscription, string payload = null,
- Dictionary options = null);
+ ///
+ /// To send a push notification call this method with a subscription, optional payload and any options
+ /// Will exception if unsuccessful
+ ///
+ /// The PushSubscription you wish to send the notification to.
+ /// The payload you wish to send to the user
+ ///
+ /// Options for the web push request, including vapid keys, if they are unique for each
+ /// notification.
+ ///
+ void SendNotification(PushSubscription subscription, string? payload = null, WebPushOptions? options = null);
- ///
- /// To send a push notification call this method with a subscription, optional payload and any options
- /// Will exception if unsuccessful
- ///
- /// The PushSubscription you wish to send the notification to.
- /// The payload you wish to send to the user
- /// The vapid details for the notification.
- void SendNotification(PushSubscription subscription, string payload, VapidDetails vapidDetails);
+ ///
+ /// To send a push notification call this method with a subscription, optional payload and any options
+ /// Will exception if unsuccessful
+ ///
+ /// The PushSubscription you wish to send the notification to.
+ /// The payload you wish to send to the user
+ ///
+ /// Options for vapid keys can be passed in if they are unique for each
+ /// notification.
+ ///
+ [Obsolete("Use SendNotification with WebPushOptions")]
+ void SendNotification(PushSubscription subscription, string? payload = null,
+ Dictionary? options = null);
- ///
- /// To send a push notification call this method with a subscription, optional payload and any options
- /// Will exception if unsuccessful
- ///
- /// The PushSubscription you wish to send the notification to.
- /// The payload you wish to send to the user
- /// The GCM API key
- void SendNotification(PushSubscription subscription, string payload, string gcmApiKey);
+ ///
+ /// To send a push notification call this method with a subscription, optional payload and any options
+ /// Will exception if unsuccessful
+ ///
+ /// The PushSubscription you wish to send the notification to.
+ /// The payload you wish to send to the user
+ /// The vapid details for the notification.
+ void SendNotification(PushSubscription subscription, string payload, VapidDetails vapidDetails);
- ///
- /// To send a push notification asynchronous call this method with a subscription, optional payload and any options
- /// Will exception if unsuccessful
- ///
- /// The PushSubscription you wish to send the notification to.
- /// The payload you wish to send to the user
- ///
- /// Options for the GCM API key and vapid keys can be passed in if they are unique for each
- /// notification.
- ///
- /// The cancellation token to cancel operation.
- Task SendNotificationAsync(PushSubscription subscription, string payload = null,
- Dictionary options = null, CancellationToken cancellationToken=default);
+ ///
+ /// To send a push notification asynchronous call this method with a subscription, optional payload and any options
+ /// Will exception if unsuccessful
+ ///
+ /// The PushSubscription you wish to send the notification to.
+ /// The payload you wish to send to the user
+ ///
+ /// Options for the web push request, including vapid keys, if they are unique for each
+ /// notification.
+ ///
+ /// The cancellation token to cancel operation.
+ Task SendNotificationAsync(PushSubscription subscription, string? payload = null, WebPushOptions? options = null, CancellationToken cancellationToken = default);
- ///
- /// To send a push notification asynchronous call this method with a subscription, optional payload and any options
- /// Will exception if unsuccessful
- ///
- /// The PushSubscription you wish to send the notification to.
- /// The payload you wish to send to the user
- /// The vapid details for the notification.
- ///
- Task SendNotificationAsync(PushSubscription subscription, string payload,
- VapidDetails vapidDetails, CancellationToken cancellationToken=default);
+ ///
+ /// To send a push notification asynchronous call this method with a subscription, optional payload and any options
+ /// Will exception if unsuccessful
+ ///
+ /// The PushSubscription you wish to send the notification to.
+ /// The payload you wish to send to the user
+ ///
+ /// Options for vapid keys can be passed in if they are unique for each
+ /// notification.
+ ///
+ /// The cancellation token to cancel operation.
+ [Obsolete("Use SendNotificationAsync with WebPushOptions")]
+ Task SendNotificationAsync(PushSubscription subscription, string? payload = null,
+ Dictionary? options = null, CancellationToken cancellationToken = default);
- ///
- /// To send a push notification asynchronous call this method with a subscription, optional payload and any options
- /// Will exception if unsuccessful
- ///
- /// The PushSubscription you wish to send the notification to.
- /// The payload you wish to send to the user
- /// The GCM API key
- ///
- Task SendNotificationAsync(PushSubscription subscription, string payload, string gcmApiKey, CancellationToken cancellationToken=default);
- }
+ ///
+ /// To send a push notification asynchronous call this method with a subscription, optional payload and any options
+ /// Will exception if unsuccessful
+ ///
+ /// The PushSubscription you wish to send the notification to.
+ /// The payload you wish to send to the user
+ /// The vapid details for the notification.
+ ///
+ Task SendNotificationAsync(PushSubscription subscription, string payload,
+ VapidDetails vapidDetails, CancellationToken cancellationToken = default);
}
\ No newline at end of file
diff --git a/WebPush/Model/ContentEncoding.cs b/WebPush/Model/ContentEncoding.cs
new file mode 100644
index 0000000..27a58e2
--- /dev/null
+++ b/WebPush/Model/ContentEncoding.cs
@@ -0,0 +1,7 @@
+namespace WebPush;
+
+public enum ContentEncoding
+{
+ Aesgcm,
+ Aes128gcm,
+}
diff --git a/WebPush/Model/EncryptionResult.cs b/WebPush/Model/EncryptionResult.cs
index 1b5e9a7..2fed814 100644
--- a/WebPush/Model/EncryptionResult.cs
+++ b/WebPush/Model/EncryptionResult.cs
@@ -1,23 +1,22 @@
-using WebPush.Util;
+using Microsoft.IdentityModel.Tokens;
-namespace WebPush
+namespace WebPush.Model;
+
+// @LogicSoftware
+// Originally From: https://github.com/LogicSoftware/WebPushEncryption/blob/master/src/EncryptionResult.cs
+public class EncryptionResult
{
- // @LogicSoftware
- // Originally From: https://github.com/LogicSoftware/WebPushEncryption/blob/master/src/EncryptionResult.cs
- public class EncryptionResult
- {
- public byte[] PublicKey { get; set; }
- public byte[] Payload { get; set; }
- public byte[] Salt { get; set; }
+ public required byte[] PublicKey { get; set; }
+ public required byte[] Payload { get; set; }
+ public required byte[] Salt { get; set; }
- public string Base64EncodePublicKey()
- {
- return UrlBase64.Encode(PublicKey);
- }
+ public string Base64EncodePublicKey()
+ {
+ return Base64UrlEncoder.Encode(PublicKey);
+ }
- public string Base64EncodeSalt()
- {
- return UrlBase64.Encode(Salt);
- }
+ public string Base64EncodeSalt()
+ {
+ return Base64UrlEncoder.Encode(Salt);
}
-}
\ No newline at end of file
+}
diff --git a/WebPush/Model/InvalidEncryptionDetailsException.cs b/WebPush/Model/InvalidEncryptionDetailsException.cs
index 814b379..dd46852 100644
--- a/WebPush/Model/InvalidEncryptionDetailsException.cs
+++ b/WebPush/Model/InvalidEncryptionDetailsException.cs
@@ -1,15 +1,14 @@
using System;
-namespace WebPush.Model
+namespace WebPush.Model;
+
+public class InvalidEncryptionDetailsException : Exception
{
- public class InvalidEncryptionDetailsException : Exception
+ public InvalidEncryptionDetailsException(string message, PushSubscription pushSubscription)
+ : base(message)
{
- public InvalidEncryptionDetailsException(string message, PushSubscription pushSubscription)
- : base(message)
- {
- PushSubscription = pushSubscription;
- }
-
- public PushSubscription PushSubscription { get; }
+ PushSubscription = pushSubscription;
}
+
+ public PushSubscription PushSubscription { get; }
}
diff --git a/WebPush/Model/PushSubscription.cs b/WebPush/Model/PushSubscription.cs
index 98587c0..8816f73 100644
--- a/WebPush/Model/PushSubscription.cs
+++ b/WebPush/Model/PushSubscription.cs
@@ -1,20 +1,15 @@
-namespace WebPush
+namespace WebPush;
+
+public class PushSubscription
{
- public class PushSubscription
+ public PushSubscription(string endpoint, string p256dh, string auth)
{
- public PushSubscription()
- {
- }
-
- public PushSubscription(string endpoint, string p256dh, string auth)
- {
- Endpoint = endpoint;
- P256DH = p256dh;
- Auth = auth;
- }
-
- public string Endpoint { get; set; }
- public string P256DH { get; set; }
- public string Auth { get; set; }
+ Endpoint = endpoint;
+ P256DH = p256dh;
+ Auth = auth;
}
-}
\ No newline at end of file
+
+ public string Endpoint { get; set; }
+ public string P256DH { get; set; }
+ public string Auth { get; set; }
+}
diff --git a/WebPush/Model/VapidDetails.cs b/WebPush/Model/VapidDetails.cs
index 1478216..82e5e89 100644
--- a/WebPush/Model/VapidDetails.cs
+++ b/WebPush/Model/VapidDetails.cs
@@ -1,25 +1,23 @@
-namespace WebPush
-{
- public class VapidDetails
- {
- public VapidDetails()
- {
- }
-
- /// This should be a URL or a 'mailto:' email address
- /// The VAPID public key as a base64 encoded string
- /// The VAPID private key as a base64 encoded string
- public VapidDetails(string subject, string publicKey, string privateKey)
- {
- Subject = subject;
- PublicKey = publicKey;
- PrivateKey = privateKey;
- }
+using System;
+using System.Diagnostics.CodeAnalysis;
- public string Subject { get; set; }
- public string PublicKey { get; set; }
- public string PrivateKey { get; set; }
+namespace WebPush;
- public long Expiration { get; set; } = -1;
+public class VapidDetails
+{
+ /// This should be a URL or a 'mailto:' email address
+ /// The VAPID public key as a base64 encoded string
+ /// The VAPID private key as a base64 encoded string
+ [SetsRequiredMembers]
+ public VapidDetails(string subject, string publicKey, string privateKey)
+ {
+ Subject = subject;
+ PublicKey = publicKey;
+ PrivateKey = privateKey;
}
-}
\ No newline at end of file
+
+ public required string Subject { get; set; }
+ public required string PublicKey { get; set; }
+ public required string PrivateKey { get; set; }
+ public DateTime? Expiration { get; set; } = null;
+}
diff --git a/WebPush/Model/WebPushException.cs b/WebPush/Model/WebPushException.cs
index 6fa5cb6..86b977d 100644
--- a/WebPush/Model/WebPushException.cs
+++ b/WebPush/Model/WebPushException.cs
@@ -3,20 +3,19 @@
using System.Net.Http;
using System.Net.Http.Headers;
-namespace WebPush
+namespace WebPush.Model;
+
+public class WebPushException : Exception
{
- public class WebPushException : Exception
+ public WebPushException(string message, PushSubscription pushSubscription, HttpResponseMessage responseMessage) : base(message)
{
- public WebPushException(string message, PushSubscription pushSubscription, HttpResponseMessage responseMessage) : base(message)
- {
- PushSubscription = pushSubscription;
- HttpResponseMessage = responseMessage;
- }
+ PushSubscription = pushSubscription;
+ HttpResponseMessage = responseMessage;
+ }
- public HttpStatusCode StatusCode => HttpResponseMessage.StatusCode;
+ public HttpStatusCode StatusCode => HttpResponseMessage.StatusCode;
- public HttpResponseHeaders Headers => HttpResponseMessage.Headers;
- public PushSubscription PushSubscription { get; set; }
- public HttpResponseMessage HttpResponseMessage { get; set; }
- }
-}
\ No newline at end of file
+ public HttpResponseHeaders Headers => HttpResponseMessage.Headers;
+ public PushSubscription PushSubscription { get; set; }
+ public HttpResponseMessage HttpResponseMessage { get; set; }
+}
diff --git a/WebPush/Model/WebPushOptions.cs b/WebPush/Model/WebPushOptions.cs
new file mode 100644
index 0000000..3b9cbda
--- /dev/null
+++ b/WebPush/Model/WebPushOptions.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+
+namespace WebPush;
+
+public class WebPushOptions
+{
+ public VapidDetails? VapidDetails { get; set; }
+ public const int DefaultTtl = 2419200; // default is 4 weeks
+ public int TTL { get; set; } = DefaultTtl;
+ public const ContentEncoding DefaultContentEncoding = WebPush.ContentEncoding.Aes128gcm;
+ public ContentEncoding? ContentEncoding { get; set; }
+ public Urgency? Urgency { get; set; }
+ public string? Topic { get; set; }
+ public Dictionary? ExtraHeaders { get; set; }
+
+ // 'proxy',
+ // 'agent',
+ // 'timeout'
+}
diff --git a/WebPush/Urgency.cs b/WebPush/Urgency.cs
new file mode 100644
index 0000000..dc59019
--- /dev/null
+++ b/WebPush/Urgency.cs
@@ -0,0 +1,9 @@
+namespace WebPush;
+
+public enum Urgency
+{
+ VeryLow,
+ Low,
+ Normal,
+ High,
+}
diff --git a/WebPush/Util/ECKeyHelper.cs b/WebPush/Util/ECKeyHelper.cs
index 5655908..af809f7 100644
--- a/WebPush/Util/ECKeyHelper.cs
+++ b/WebPush/Util/ECKeyHelper.cs
@@ -1,64 +1,78 @@
using System;
-using System.IO;
-using Org.BouncyCastle.Asn1;
-using Org.BouncyCastle.Asn1.Nist;
-using Org.BouncyCastle.Crypto;
-using Org.BouncyCastle.Crypto.Parameters;
-using Org.BouncyCastle.OpenSsl;
-using Org.BouncyCastle.Security;
+using System.Linq;
+using System.Security.Cryptography;
+using Microsoft.IdentityModel.Tokens;
-namespace WebPush.Util
+namespace WebPush.Util;
+
+internal static class ECKeyHelper
{
- internal static class ECKeyHelper
+ public static ECDsa GetKeyPair(byte[] privateKey, byte[] publicKey)
{
- public static ECPrivateKeyParameters GetPrivateKey(byte[] privateKey)
+ return ECDsa.Create(new ECParameters
{
- Asn1Object version = new DerInteger(1);
- Asn1Object derEncodedKey = new DerOctetString(privateKey);
- Asn1Object keyTypeParameters = new DerTaggedObject(0, new DerObjectIdentifier(@"1.2.840.10045.3.1.7"));
-
- Asn1Object derSequence = new DerSequence(version, derEncodedKey, keyTypeParameters);
-
- var base64EncodedDerSequence = Convert.ToBase64String(derSequence.GetDerEncoded());
- var pemKey = "-----BEGIN EC PRIVATE KEY-----\n";
- pemKey += base64EncodedDerSequence;
- pemKey += "\n-----END EC PRIVATE KEY----";
+ Curve = ECCurve.NamedCurves.nistP256,
+ D = privateKey,
+ Q = new ECPoint
+ {
+ X = [.. publicKey.Skip(1).Take(32)],
+ Y = [.. publicKey.Skip(33)],
+ }
+ });
+ }
- var reader = new StringReader(pemKey);
- var pemReader = new PemReader(reader);
- var keyPair = (AsymmetricCipherKeyPair) pemReader.ReadObject();
+ public static byte[] GetPublicKey(this ECDsa keypair)
+ {
+ var ep = keypair.ExportParameters(includePrivateParameters: true);
+ if (ep.Q.X is null || ep.Q.Y is null) throw new InvalidOperationException();
+ return [4, .. ep.Q.X, .. ep.Q.Y];
+ }
- return (ECPrivateKeyParameters) keyPair.Private;
- }
- public static ECPublicKeyParameters GetPublicKey(byte[] publicKey)
- {
- Asn1Object keyTypeParameters = new DerSequence(new DerObjectIdentifier(@"1.2.840.10045.2.1"),
- new DerObjectIdentifier(@"1.2.840.10045.3.1.7"));
- Asn1Object derEncodedKey = new DerBitString(publicKey);
+ public static byte[] GetPrivateKey(this ECDsa keypair)
+ {
+ var ep = keypair.ExportParameters(includePrivateParameters: true);
+ if (ep.D is null) throw new InvalidOperationException();
+ return [.. ep.D];
+ }
- Asn1Object derSequence = new DerSequence(keyTypeParameters, derEncodedKey);
+ public static string GetEncodedPublicKey(this ECDsa keypair) => Base64UrlEncoder.Encode(keypair.GetPublicKey());
+ public static string GetEncodedPrivateKey(this ECDsa keypair) => Base64UrlEncoder.Encode(keypair.GetPrivateKey());
- var base64EncodedDerSequence = Convert.ToBase64String(derSequence.GetDerEncoded());
- var pemKey = "-----BEGIN PUBLIC KEY-----\n";
- pemKey += base64EncodedDerSequence;
- pemKey += "\n-----END PUBLIC KEY-----";
+ public static ECDsa GenerateKeys()
+ {
+ return ECDsa.Create(ECCurve.NamedCurves.nistP256);
+ }
- var reader = new StringReader(pemKey);
- var pemReader = new PemReader(reader);
- var keyPair = pemReader.ReadObject();
- return (ECPublicKeyParameters) keyPair;
- }
+ public static byte[] GetECDiffieHellmanSharedKey(byte[] privateKey, byte[] publicKey)
+ {
+ var myKey = CreateWithPrivateKey(privateKey);
+ var otherKey = CreateWithPublicKey(publicKey);
+ return myKey.DeriveRawSecretAgreement(otherKey.PublicKey);
+ }
- public static AsymmetricCipherKeyPair GenerateKeys()
+ internal static ECDiffieHellman CreateWithPrivateKey(byte[] privateKey)
+ {
+ var parameters = new ECParameters
{
- var ecParameters = NistNamedCurves.GetByName("P-256");
- var ecSpec = new ECDomainParameters(ecParameters.Curve, ecParameters.G, ecParameters.N, ecParameters.H,
- ecParameters.GetSeed());
- var keyPairGenerator = GeneratorUtilities.GetKeyPairGenerator("ECDH");
- keyPairGenerator.Init(new ECKeyGenerationParameters(ecSpec, new SecureRandom()));
+ Curve = ECCurve.NamedCurves.nistP256,
+ D = privateKey,
+ };
+ return ECDiffieHellman.Create(parameters);
+ }
- return keyPairGenerator.GenerateKeyPair();
- }
+ internal static ECDiffieHellman CreateWithPublicKey(byte[] publicKey)
+ {
+ var parameters = new ECParameters
+ {
+ Curve = ECCurve.NamedCurves.nistP256,
+ Q = new ECPoint
+ {
+ X = [.. publicKey.Skip(1).Take(32)],
+ Y = [.. publicKey.Skip(33)],
+ },
+ };
+ return ECDiffieHellman.Create(parameters);
}
-}
\ No newline at end of file
+
+}
diff --git a/WebPush/Util/Encryptor.cs b/WebPush/Util/Encryptor.cs
index d8b294f..ed1d3ea 100644
--- a/WebPush/Util/Encryptor.cs
+++ b/WebPush/Util/Encryptor.cs
@@ -1,150 +1,105 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
using System.Text;
-using Org.BouncyCastle.Crypto.Digests;
-using Org.BouncyCastle.Crypto.Engines;
-using Org.BouncyCastle.Crypto.Macs;
-using Org.BouncyCastle.Crypto.Modes;
-using Org.BouncyCastle.Crypto.Parameters;
-using Org.BouncyCastle.Security;
-
-namespace WebPush.Util
-{
- // @LogicSoftware
- // Originally from https://github.com/LogicSoftware/WebPushEncryption/blob/master/src/Encryptor.cs
- internal static class Encryptor
- {
- public static EncryptionResult Encrypt(string userKey, string userSecret, string payload)
- {
- var userKeyBytes = UrlBase64.Decode(userKey);
- var userSecretBytes = UrlBase64.Decode(userSecret);
- var payloadBytes = Encoding.UTF8.GetBytes(payload);
-
- return Encrypt(userKeyBytes, userSecretBytes, payloadBytes);
- }
+using Microsoft.IdentityModel.Tokens;
+using WebPush.Model;
- public static EncryptionResult Encrypt(byte[] userKey, byte[] userSecret, byte[] payload)
- {
- var salt = GenerateSalt(16);
- var serverKeyPair = ECKeyHelper.GenerateKeys();
+[assembly: InternalsVisibleTo("WebPush.Test")]
- var ecdhAgreement = AgreementUtilities.GetBasicAgreement("ECDH");
- ecdhAgreement.Init(serverKeyPair.Private);
+namespace WebPush.Util;
- var userPublicKey = ECKeyHelper.GetPublicKey(userKey);
+internal static class Encryptor
+{
+ public static EncryptionResult Encrypt(string subscriptionPublicKeyBase64, string authSecretBase64, string payload)
+ {
+ var subscriptionPublicKey = Base64UrlEncoder.DecodeBytes(subscriptionPublicKeyBase64);
+ var authenticationSecret = Base64UrlEncoder.DecodeBytes(authSecretBase64);
- var key = ecdhAgreement.CalculateAgreement(userPublicKey).ToByteArrayUnsigned();
- var serverPublicKey = ((ECPublicKeyParameters) serverKeyPair.Public).Q.GetEncoded(false);
+ // see https://datatracker.ietf.org/doc/html/rfc8291
- var prk = HKDF(userSecret, key, Encoding.UTF8.GetBytes("Content-Encoding: auth\0"), 32);
- var cek = HKDF(salt, prk, CreateInfoChunk("aesgcm", userKey, serverPublicKey), 16);
- var nonce = HKDF(salt, prk, CreateInfoChunk("nonce", userKey, serverPublicKey), 12);
+ using var ephemeralEcdh = ECKeyHelper.GenerateKeys();
+ var uncompressedEphemeralPublicKey = ephemeralEcdh.GetPublicKey();
+ var sharedSecret = ECKeyHelper.GetECDiffieHellmanSharedKey(ephemeralEcdh.GetPrivateKey(), subscriptionPublicKey);
- var input = AddPaddingToInput(payload);
- var encryptedMessage = EncryptAes(nonce, cek, input);
+ Span salt = stackalloc byte[16];
+ RandomNumberGenerator.Fill(salt);
- return new EncryptionResult
- {
- Salt = salt,
- Payload = encryptedMessage,
- PublicKey = serverPublicKey
- };
- }
+ // Step 0 PRK_key
+ Span prkkey = stackalloc byte[32]; // SHA256 output is 32 bytes
+ HKDF.Extract(HashAlgorithmName.SHA256, sharedSecret, authenticationSecret, prkkey);
- private static byte[] GenerateSalt(int length)
- {
- var salt = new byte[length];
- var random = new Random();
- random.NextBytes(salt);
- return salt;
- }
+ // Step 1 IKM
+ byte[] keyInfo = [.. Encoding.UTF8.GetBytes("WebPush: info"), 0x00, .. subscriptionPublicKey, .. uncompressedEphemeralPublicKey];
+ Span ikm = stackalloc byte[32];
+ HKDF.Expand(HashAlgorithmName.SHA256, prkkey, ikm, keyInfo);
- private static byte[] AddPaddingToInput(byte[] data)
- {
- var input = new byte[0 + 2 + data.Length];
- Buffer.BlockCopy(ConvertInt(0), 0, input, 0, 2);
- Buffer.BlockCopy(data, 0, input, 0 + 2, data.Length);
- return input;
- }
+ // Step 2 PRK
+ Span prk = stackalloc byte[32];
+ HKDF.Extract(HashAlgorithmName.SHA256, ikm, salt, prk);
- private static byte[] EncryptAes(byte[] nonce, byte[] cek, byte[] message)
- {
- var cipher = new GcmBlockCipher(new AesEngine());
- var parameters = new AeadParameters(new KeyParameter(cek), 128, nonce);
- cipher.Init(true, parameters);
+ // Step 3 CEK
+ byte[] cekInfo = [.. Encoding.UTF8.GetBytes("Content-Encoding: aes128gcm"), 0x00];
+ Span cek = stackalloc byte[16];
+ HKDF.Expand(HashAlgorithmName.SHA256, prk, cek, cekInfo);
- //Generate Cipher Text With Auth Tag
- var cipherText = new byte[cipher.GetOutputSize(message.Length)];
- var len = cipher.ProcessBytes(message, 0, message.Length, cipherText, 0);
- cipher.DoFinal(cipherText, len);
+ // Step 4 NONCE
+ byte[] nonceInfo = [.. Encoding.UTF8.GetBytes("Content-Encoding: nonce"), 0x00];
+ Span nonce = stackalloc byte[12];
+ HKDF.Expand(HashAlgorithmName.SHA256, prk, nonce, nonceInfo);
- //byte[] tag = cipher.GetMac();
- return cipherText;
- }
+ // Step 5 Header
+ var maxContentLength = BitConverter.GetBytes(Convert.ToInt32(4096));
+ if (BitConverter.IsLittleEndian) { Array.Reverse(maxContentLength); }
+ var asPublicLength = Convert.ToByte(uncompressedEphemeralPublicKey.Length);
+ byte[] header = [.. salt, .. maxContentLength, asPublicLength, .. uncompressedEphemeralPublicKey];
- public static byte[] HKDFSecondStep(byte[] key, byte[] info, int length)
- {
- var hmac = new HmacSha256(key);
- var infoAndOne = info.Concat(new byte[] {0x01}).ToArray();
- var result = hmac.ComputeHash(infoAndOne);
+ // Step 6 Payload padding
+ byte[] paddedPayload = [.. Encoding.UTF8.GetBytes(payload), 0x02];
- if (result.Length > length)
- {
- Array.Resize(ref result, length);
- }
+ // Step 7 Content Encryption
+ var cipherText = EncryptMessage(paddedPayload, [.. cek], [.. nonce]);
+ byte[] encryptedContent = [.. header, .. cipherText];
- return result;
- }
-
- public static byte[] HKDF(byte[] salt, byte[] prk, byte[] info, int length)
- {
- var hmac = new HmacSha256(salt);
- var key = hmac.ComputeHash(prk);
-
- return HKDFSecondStep(key, info, length);
- }
-
- public static byte[] ConvertInt(int number)
+ return new EncryptionResult
{
- var output = BitConverter.GetBytes(Convert.ToUInt16(number));
- if (BitConverter.IsLittleEndian)
- {
- Array.Reverse(output);
- }
-
- return output;
- }
-
- public static byte[] CreateInfoChunk(string type, byte[] recipientPublicKey, byte[] senderPublicKey)
- {
- var output = new List();
- output.AddRange(Encoding.UTF8.GetBytes($"Content-Encoding: {type}\0P-256\0"));
- output.AddRange(ConvertInt(recipientPublicKey.Length));
- output.AddRange(recipientPublicKey);
- output.AddRange(ConvertInt(senderPublicKey.Length));
- output.AddRange(senderPublicKey);
- return output.ToArray();
- }
+ Salt = [.. salt],
+ Payload = encryptedContent,
+ PublicKey = uncompressedEphemeralPublicKey
+ };
}
- public class HmacSha256
+ ///
+ /// Encrypts a byte array using AES with a given key and a new random IV.
+ ///
+ /// The Web Push protocol specifies a 16-byte authentication tag.
+ ///
+ public static byte[] EncryptMessage(byte[] payload, byte[] key, byte[] iv)
{
- private readonly HMac _hmac;
+ var tag = new byte[AesGcm.TagByteSizes.MaxSize];
+ var encryptedBytes = new byte[payload.Length];
- public HmacSha256(byte[] key)
+ using (var aesGcm = new AesGcm(key, AesGcm.TagByteSizes.MaxSize))
{
- _hmac = new HMac(new Sha256Digest());
- _hmac.Init(new KeyParameter(key));
+ aesGcm.Encrypt(iv, payload, encryptedBytes, tag);
}
- public byte[] ComputeHash(byte[] value)
- {
- var resBuf = new byte[_hmac.GetMacSize()];
- _hmac.BlockUpdate(value, 0, value.Length);
- _hmac.DoFinal(resBuf, 0);
+ return [.. encryptedBytes, .. tag];
+ }
- return resBuf;
- }
+ ///
+ /// Decrypts a byte array using AES with a given key and IV.
+ ///
+ /// ciphertext must contain the tag as the end (last 16 bytes).
+ ///
+ public static string DecryptMessage(byte[] payload, byte[] key, byte[] nonce)
+ {
+ ReadOnlySpan readOnlySpan = payload;
+ var tag = readOnlySpan.Slice(payload.Length - AesGcm.TagByteSizes.MaxSize, length: AesGcm.TagByteSizes.MaxSize);
+ var ciphertext = readOnlySpan.Slice(0, payload.Length - AesGcm.TagByteSizes.MaxSize);
+ using var aes = new AesGcm(key, tag.Length);
+ var plaintextBytes = new byte[ciphertext.Length];
+ aes.Decrypt(nonce, ciphertext, tag, plaintextBytes);
+ return Encoding.UTF8.GetString(plaintextBytes);
}
-}
\ No newline at end of file
+}
diff --git a/WebPush/Util/EnumHelper.cs b/WebPush/Util/EnumHelper.cs
new file mode 100644
index 0000000..0739a4c
--- /dev/null
+++ b/WebPush/Util/EnumHelper.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Globalization;
+using System.Text.RegularExpressions;
+
+namespace WebPush.Util;
+
+public static partial class EnumHelper2
+{
+
+ public static string ToKebabCaseLower(this T val) where T : Enum
+ {
+ return RegexVariableName().Replace(val.ToString()!, "${first}-${remainder}").ToLower(CultureInfo.InvariantCulture);
+ }
+
+ [GeneratedRegex("(?[a-z0-9]|(?<=[a-z0-9]))(?[A-Z])", RegexOptions.None, matchTimeoutMilliseconds: 200)]
+ private static partial Regex RegexVariableName();
+}
+
diff --git a/WebPush/Util/JwsSigner.cs b/WebPush/Util/JwsSigner.cs
deleted file mode 100644
index f7aaf52..0000000
--- a/WebPush/Util/JwsSigner.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Text.Json;
-using Org.BouncyCastle.Crypto.Digests;
-using Org.BouncyCastle.Crypto.Parameters;
-using Org.BouncyCastle.Crypto.Signers;
-
-namespace WebPush.Util
-{
- internal class JwsSigner
- {
- private readonly ECPrivateKeyParameters _privateKey;
-
- public JwsSigner(ECPrivateKeyParameters privateKey)
- {
- _privateKey = privateKey;
- }
-
- ///
- /// Generates a Jws Signature.
- ///
- ///
- ///
- ///
- public string GenerateSignature(Dictionary header, Dictionary payload)
- {
- var securedInput = SecureInput(header, payload);
- var message = Encoding.UTF8.GetBytes(securedInput);
-
- var hashedMessage = Sha256Hash(message);
-
- var signer = new ECDsaSigner();
- signer.Init(true, _privateKey);
- var results = signer.GenerateSignature(hashedMessage);
-
- // Concated to create signature
- var a = results[0].ToByteArrayUnsigned();
- var b = results[1].ToByteArrayUnsigned();
-
- // a,b are required to be exactly the same length of bytes
- if (a.Length != b.Length)
- {
- var largestLength = Math.Max(a.Length, b.Length);
- a = ByteArrayPadLeft(a, largestLength);
- b = ByteArrayPadLeft(b, largestLength);
- }
-
- var signature = UrlBase64.Encode(a.Concat(b).ToArray());
- return $"{securedInput}.{signature}";
- }
-
- private static string SecureInput(Dictionary header, Dictionary payload)
- {
- var encodeHeader = UrlBase64.Encode(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(header)));
- var encodePayload = UrlBase64.Encode(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(payload)));
-
- return $"{encodeHeader}.{encodePayload}";
- }
-
- private static byte[] ByteArrayPadLeft(byte[] src, int size)
- {
- var dst = new byte[size];
- var startAt = dst.Length - src.Length;
- Array.Copy(src, 0, dst, startAt, src.Length);
- return dst;
- }
-
- private static byte[] Sha256Hash(byte[] message)
- {
- var sha256Digest = new Sha256Digest();
- sha256Digest.BlockUpdate(message, 0, message.Length);
- var hash = new byte[sha256Digest.GetDigestSize()];
- sha256Digest.DoFinal(hash, 0);
- return hash;
- }
- }
-}
diff --git a/WebPush/Util/UrlBase64.cs b/WebPush/Util/UrlBase64.cs
deleted file mode 100644
index 1e61563..0000000
--- a/WebPush/Util/UrlBase64.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System;
-
-namespace WebPush.Util
-{
- internal static class UrlBase64
- {
- ///
- /// Decodes a url-safe base64 string into bytes
- ///
- ///
- ///
- public static byte[] Decode(string base64)
- {
- base64 = base64.Replace('-', '+').Replace('_', '/');
-
- while (base64.Length % 4 != 0)
- {
- base64 += "=";
- }
-
- return Convert.FromBase64String(base64);
- }
-
- ///
- /// Encodes bytes into url-safe base64 string
- ///
- ///
- ///
- public static string Encode(byte[] data)
- {
- return Convert.ToBase64String(data).Replace('+', '-').Replace('/', '_').TrimEnd('=');
- }
- }
-}
\ No newline at end of file
diff --git a/WebPush/VapidHelper.cs b/WebPush/VapidHelper.cs
index 56d1276..10b77ac 100644
--- a/WebPush/VapidHelper.cs
+++ b/WebPush/VapidHelper.cs
@@ -1,166 +1,143 @@
using System;
using System.Collections.Generic;
-using Org.BouncyCastle.Crypto.Parameters;
+using System.Security.Claims;
+using Microsoft.IdentityModel.JsonWebTokens;
+using Microsoft.IdentityModel.Tokens;
using WebPush.Util;
-namespace WebPush
+namespace WebPush;
+
+public static class VapidHelper
{
- public static class VapidHelper
+ ///
+ /// Generate vapid keys
+ ///
+ public static VapidDetails GenerateVapidKeys()
{
- ///
- /// Generate vapid keys
- ///
- public static VapidDetails GenerateVapidKeys()
- {
- var results = new VapidDetails();
-
- var keys = ECKeyHelper.GenerateKeys();
- var publicKey = ((ECPublicKeyParameters) keys.Public).Q.GetEncoded(false);
- var privateKey = ((ECPrivateKeyParameters) keys.Private).D.ToByteArrayUnsigned();
-
- results.PublicKey = UrlBase64.Encode(publicKey);
- results.PrivateKey = UrlBase64.Encode(ByteArrayPadLeft(privateKey, 32));
+ var keys = ECKeyHelper.GenerateKeys();
+ return new VapidDetails("", keys.GetEncodedPublicKey(), keys.GetEncodedPrivateKey());
+ }
- return results;
+ ///
+ /// 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.
+ /// This should be a URL or a 'mailto:' email address
+ /// The VAPID public key as a base64 encoded string
+ /// The VAPID private key as a base64 encoded string
+ /// The expiration of the VAPID JWT.
+ /// 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)
+ {
+ ValidateAudience(audience);
+ ValidateSubject(subject);
+ ValidatePublicKey(publicKey);
+ ValidatePrivateKey(privateKey);
+ var now = DateTime.UtcNow;
+ if (expiration is null)
+ {
+ expiration = now.AddHours(12);
}
-
- ///
- /// 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.
- /// This should be a URL or a 'mailto:' email address
- /// The VAPID public key as a base64 encoded string
- /// The VAPID private key as a base64 encoded string
- /// The expiration of the VAPID JWT.
- /// A dictionary of header key/value pairs.
- public static Dictionary GetVapidHeaders(string audience, string subject, string publicKey,
- string privateKey, long expiration = -1)
+ else
{
- ValidateAudience(audience);
- ValidateSubject(subject);
- ValidatePublicKey(publicKey);
- ValidatePrivateKey(privateKey);
-
- var decodedPrivateKey = UrlBase64.Decode(privateKey);
-
- if (expiration == -1)
- {
- expiration = UnixTimeNow() + 43200;
- }
- else
- {
- ValidateExpiration(expiration);
- }
-
-
- var header = new Dictionary {{"typ", "JWT"}, {"alg", "ES256"}};
-
- var jwtPayload = new Dictionary {{"aud", audience}, {"exp", expiration}, {"sub", subject}};
-
- var signingKey = ECKeyHelper.GetPrivateKey(decodedPrivateKey);
-
- var signer = new JwsSigner(signingKey);
- var token = signer.GenerateSignature(header, jwtPayload);
-
- var results = new Dictionary
- {
- {"Authorization", "WebPush " + token}, {"Crypto-Key", "p256ecdsa=" + publicKey}
- };
-
- return results;
+ ValidateExpiration(expiration);
}
- public static void ValidateAudience(string audience)
+ var key = ECKeyHelper.GetKeyPair(Base64UrlEncoder.DecodeBytes(privateKey), Base64UrlEncoder.DecodeBytes(publicKey));
+ var identity = new ClaimsIdentity([new Claim(JwtRegisteredClaimNames.Sub, subject)]);
+ var handler = new JsonWebTokenHandler
+ {
+ SetDefaultTimesOnTokenCreation = false,
+ };
+ string token = handler.CreateToken(new SecurityTokenDescriptor
+ {
+ Audience = audience,
+ Expires = expiration,
+ // IssuedAt = now,
+ Subject = identity,
+ SigningCredentials = new SigningCredentials(new ECDsaSecurityKey(key), SecurityAlgorithms.EcdsaSha256),
+ });
+ return contentEncoding switch
{
- if (string.IsNullOrEmpty(audience))
+ ContentEncoding.Aesgcm => new Dictionary(StringComparer.Ordinal)
{
- throw new ArgumentException(@"No audience could be generated for VAPID.");
- }
-
- if (audience.Length == 0)
+ { "Authorization", $"WebPush {token}"},
+ { "Crypto-Key", $"p256ecdsa={publicKey}"},
+ },
+ ContentEncoding.Aes128gcm => new Dictionary(StringComparer.Ordinal)
{
- throw new ArgumentException(
- @"The audience value must be a string containing the origin of a push service. " + audience);
- }
+ { "Authorization", $"vapid t={token}, k={publicKey}"},
+ },
+ _ => throw new Exception("This content encoding is not supported"),
+ };
+ }
- if (!Uri.IsWellFormedUriString(audience, UriKind.Absolute))
- {
- throw new ArgumentException(@"VAPID audience is not a url.");
- }
+ public static void ValidateAudience(string audience)
+ {
+ if (string.IsNullOrWhiteSpace(audience))
+ {
+ throw new ArgumentException(
+ @$"The audience value must be a string containing the origin of a push service: {audience}", nameof(audience));
}
- public static void ValidateSubject(string subject)
+ if (!Uri.IsWellFormedUriString(audience, UriKind.Absolute))
{
- if (string.IsNullOrEmpty(subject))
- {
- throw new ArgumentException(@"A subject is required");
- }
-
- if (subject.Length == 0)
- {
- throw new ArgumentException(@"The subject value must be a string containing a url or mailto: address.");
- }
-
- if (!subject.StartsWith("mailto:"))
- {
- if (!Uri.IsWellFormedUriString(subject, UriKind.Absolute))
- {
- throw new ArgumentException(@"Subject is not a valid URL or mailto address");
- }
- }
+ throw new ArgumentException(@$"VAPID audience is not a url: {audience}", nameof(audience));
}
+ }
- public static void ValidatePublicKey(string publicKey)
+ public static void ValidateSubject(string subject)
+ {
+ if (string.IsNullOrWhiteSpace(subject))
{
- if (string.IsNullOrEmpty(publicKey))
- {
- throw new ArgumentException(@"Valid public key not set");
- }
-
- var decodedPublicKey = UrlBase64.Decode(publicKey);
+ throw new ArgumentException(@"The subject value must be a string containing a url or mailto: address.", nameof(subject));
+ }
- if (decodedPublicKey.Length != 65)
+ if (!subject.StartsWith("mailto:", StringComparison.Ordinal))
+ {
+ if (!Uri.IsWellFormedUriString(subject, UriKind.Absolute))
{
- throw new ArgumentException(@"Vapid public key must be 65 characters long when decoded");
+ throw new ArgumentException(@"Subject is not a valid URL or mailto address", nameof(subject));
}
}
+ }
- public static void ValidatePrivateKey(string privateKey)
+ public static void ValidatePublicKey(string publicKey)
+ {
+ if (string.IsNullOrWhiteSpace(publicKey))
{
- if (string.IsNullOrEmpty(privateKey))
- {
- throw new ArgumentException(@"Valid private key not set");
- }
+ throw new ArgumentException(@"Valid public key not set", nameof(publicKey));
+ }
- var decodedPrivateKey = UrlBase64.Decode(privateKey);
+ var decodedPublicKey = Base64UrlEncoder.DecodeBytes(publicKey);
- if (decodedPrivateKey.Length != 32)
- {
- throw new ArgumentException(@"Vapid private key should be 32 bytes long when decoded.");
- }
+ if (decodedPublicKey.Length != 65)
+ {
+ throw new ArgumentException(@"Vapid public key must be 65 characters long when decoded", nameof(publicKey));
}
+ }
- private static void ValidateExpiration(long expiration)
+ public static void ValidatePrivateKey(string privateKey)
+ {
+ if (string.IsNullOrWhiteSpace(privateKey))
{
- if (expiration <= UnixTimeNow())
- {
- throw new ArgumentException(@"Vapid expiration must be a unix timestamp in the future");
- }
+ throw new ArgumentException(@"Valid private key not set", nameof(privateKey));
}
- private static long UnixTimeNow()
+ var decodedPrivateKey = Base64UrlEncoder.DecodeBytes(privateKey);
+
+ if (decodedPrivateKey.Length != 32)
{
- var timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0);
- return (long) timeSpan.TotalSeconds;
+ throw new ArgumentException(@"Vapid private key should be 32 bytes long when decoded.", nameof(privateKey));
}
+ }
- private static byte[] ByteArrayPadLeft(byte[] src, int size)
+ private static void ValidateExpiration(DateTime? expiration)
+ {
+ if (expiration is null || expiration <= DateTime.UtcNow)
{
- var dst = new byte[size];
- var startAt = dst.Length - src.Length;
- Array.Copy(src, 0, dst, startAt, src.Length);
- return dst;
+ throw new ArgumentException(@"Vapid expiration must be in the future", nameof(expiration));
}
}
-}
\ No newline at end of file
+}
diff --git a/WebPush/WebPush.csproj b/WebPush/WebPush.csproj
index 191634d..2315ca1 100755
--- a/WebPush/WebPush.csproj
+++ b/WebPush/WebPush.csproj
@@ -1,9 +1,9 @@
- net48;netstandard2.1;net8.0;net9.0;net10.0
+ net8.0;net9.0;net10.0
true
- 1.0.13
+ 2.0.0
Cory Thompson
@@ -14,23 +14,18 @@
web push notifications vapid
true
true
+ enable
+ true
snupkg
README.md
-
+
+
-
-
-
-
-
-
-
-
diff --git a/WebPush/WebPushClient.cs b/WebPush/WebPushClient.cs
index df1bfe1..5782675 100644
--- a/WebPush/WebPushClient.cs
+++ b/WebPush/WebPushClient.cs
@@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using WebPush.Model;
@@ -11,231 +14,194 @@
[assembly: InternalsVisibleTo("WebPush.Test")]
-namespace WebPush
+namespace WebPush;
+
+public partial class WebPushClient : IWebPushClient
{
- public class WebPushClient : IWebPushClient
- {
- // default TTL is 4 weeks.
- private const int DefaultTtl = 2419200;
- private readonly HttpClientHandler _httpClientHandler;
+ private readonly HttpClientHandler? _httpClientHandler;
- private string _gcmApiKey;
- private HttpClient _httpClient;
- private VapidDetails _vapidDetails;
+ private HttpClient? _httpClient;
+ private VapidDetails? _vapidDetails;
- // Used so we only cleanup internally created http clients
- private bool _isHttpClientInternallyCreated;
+ // Used so we only cleanup internally created http clients
+ private bool _isHttpClientInternallyCreated;
- public WebPushClient()
- {
+ public WebPushClient()
+ {
- }
+ }
- public WebPushClient(HttpClient httpClient)
- {
- _httpClient = httpClient;
- }
+ public WebPushClient(HttpClient httpClient)
+ {
+ _httpClient = httpClient;
+ }
- public WebPushClient(HttpClientHandler httpClientHandler)
- {
- _httpClientHandler = httpClientHandler;
- }
+ public WebPushClient(HttpClientHandler httpClientHandler)
+ {
+ _httpClientHandler = httpClientHandler;
+ }
- protected HttpClient HttpClient
+ protected HttpClient HttpClient
+ {
+ get
{
- get
+ if (_httpClient != null)
{
- if (_httpClient != null)
- {
- return _httpClient;
- }
-
- _isHttpClientInternallyCreated = true;
- _httpClient = _httpClientHandler == null
- ? new HttpClient()
- : new HttpClient(_httpClientHandler);
-
return _httpClient;
}
+
+ _isHttpClientInternallyCreated = true;
+ _httpClient = _httpClientHandler == null
+ ? new HttpClient()
+ : new HttpClient(_httpClientHandler);
+
+ return _httpClient;
}
+ }
- ///
- /// When sending messages to a GCM endpoint you need to set the GCM API key
- /// by either calling setGcmApiKey() or passing in the API key as an option
- /// to sendNotification()
- ///
- /// The API key to send with the GCM request.
- public void SetGcmApiKey(string gcmApiKey)
- {
- if (gcmApiKey == null)
- {
- _gcmApiKey = null;
- return;
- }
+ ///
+ /// When marking requests where you want to define VAPID details, call this method
+ /// before sendNotifications() or pass in the details and options to
+ /// sendNotification.
+ ///
+ ///
+ public void SetVapidDetails(VapidDetails vapidDetails)
+ {
+ VapidHelper.ValidateSubject(vapidDetails.Subject);
+ VapidHelper.ValidatePublicKey(vapidDetails.PublicKey);
+ VapidHelper.ValidatePrivateKey(vapidDetails.PrivateKey);
- if (string.IsNullOrEmpty(gcmApiKey))
- {
- throw new ArgumentException(@"The GCM API Key should be a non-empty string or null.");
- }
+ _vapidDetails = vapidDetails;
+ }
- _gcmApiKey = gcmApiKey;
- }
+ ///
+ /// When marking requests where you want to define VAPID details, call this method
+ /// before sendNotifications() or pass in the details and options to
+ /// sendNotification.
+ ///
+ /// This must be either a URL or a 'mailto:' address
+ /// The public VAPID key as a base64 encoded string
+ /// The private VAPID key as a base64 encoded string
+ public void SetVapidDetails(string subject, string publicKey, string privateKey)
+ {
+ SetVapidDetails(new VapidDetails(subject, publicKey, privateKey));
+ }
- ///
- /// When marking requests where you want to define VAPID details, call this method
- /// before sendNotifications() or pass in the details and options to
- /// sendNotification.
- ///
- ///
- public void SetVapidDetails(VapidDetails vapidDetails)
- {
- VapidHelper.ValidateSubject(vapidDetails.Subject);
- VapidHelper.ValidatePublicKey(vapidDetails.PublicKey);
- VapidHelper.ValidatePrivateKey(vapidDetails.PrivateKey);
+ ///
+ /// To get a request without sending a push notification call this method.
+ /// This method will throw an ArgumentException if there is an issue with the input.
+ ///
+ /// The PushSubscription you wish to send the notification to.
+ /// The payload you wish to send to the user
+ ///
+ /// Options for vapid keys can be passed in if they are unique for each
+ /// notification.
+ ///
+ /// A HttpRequestMessage object that can be sent.
+ public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string? payload,
+ Dictionary? options = null)
+ {
+ var wpo = ConvertOptions(options);
+ return GenerateRequestDetails(subscription, payload, wpo);
+ }
- _vapidDetails = vapidDetails;
+ ///
+ /// To get a request without sending a push notification call this method.
+ /// This method will throw an ArgumentException if there is an issue with the input.
+ ///
+ /// The PushSubscription you wish to send the notification to.
+ /// The payload you wish to send to the user
+ ///
+ /// Options for the vapid keys can be passed in if they are unique for each
+ /// notification.
+ ///
+ /// A HttpRequestMessage object that can be sent.
+ public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string? payload, WebPushOptions? options = null)
+ {
+ if (!Uri.IsWellFormedUriString(subscription.Endpoint, UriKind.Absolute))
+ {
+ throw new ArgumentException(@"You must pass in a subscription with at least a valid endpoint", nameof(subscription));
}
- ///
- /// When marking requests where you want to define VAPID details, call this method
- /// before sendNotifications() or pass in the details and options to
- /// sendNotification.
- ///
- /// This must be either a URL or a 'mailto:' address
- /// The public VAPID key as a base64 encoded string
- /// The private VAPID key as a base64 encoded string
- public void SetVapidDetails(string subject, string publicKey, string privateKey)
+ var request = new HttpRequestMessage(HttpMethod.Post, subscription.Endpoint);
+
+ if (!string.IsNullOrEmpty(payload) && (string.IsNullOrEmpty(subscription.Auth) ||
+ string.IsNullOrEmpty(subscription.P256DH)))
{
- SetVapidDetails(new VapidDetails(subject, publicKey, privateKey));
+ throw new ArgumentException(
+ @"To send a message with a payload, the subscription must have 'auth' and 'p256dh' keys.", nameof(subscription));
}
- ///
- /// To get a request without sending a push notification call this method.
- /// This method will throw an ArgumentException if there is an issue with the input.
- ///
- /// The PushSubscription you wish to send the notification to.
- /// The payload you wish to send to the user
- ///
- /// Options for the GCM API key and vapid keys can be passed in if they are unique for each
- /// notification.
- ///
- /// A HttpRequestMessage object that can be sent.
- public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string payload,
- Dictionary options = null)
+ if (options is not null)
{
- if (!Uri.IsWellFormedUriString(subscription.Endpoint, UriKind.Absolute))
- {
- throw new ArgumentException(@"You must pass in a subscription with at least a valid endpoint");
- }
-
- var request = new HttpRequestMessage(HttpMethod.Post, subscription.Endpoint);
-
- if (!string.IsNullOrEmpty(payload) && (string.IsNullOrEmpty(subscription.Auth) ||
- string.IsNullOrEmpty(subscription.P256DH)))
- {
- throw new ArgumentException(
- @"To send a message with a payload, the subscription must have 'auth' and 'p256dh' keys.");
- }
-
- var currentGcmApiKey = _gcmApiKey;
- var currentVapidDetails = _vapidDetails;
- var timeToLive = DefaultTtl;
- var extraHeaders = new Dictionary();
-
- if (options != null)
+ if (options.Topic is not null)
{
- var validOptionsKeys = new List { "headers", "gcmAPIKey", "vapidDetails", "TTL" };
- foreach (var key in options.Keys)
- {
- if (!validOptionsKeys.Contains(key))
- {
- throw new ArgumentException(key + " is an invalid options. The valid options are" +
- string.Join(",", validOptionsKeys));
- }
- }
-
- if (options.ContainsKey("headers"))
- {
- var headers = options["headers"] as Dictionary;
-
- extraHeaders = headers ?? throw new ArgumentException("options.headers must be of type Dictionary");
- }
-
- if (options.ContainsKey("gcmAPIKey"))
+ if (string.IsNullOrWhiteSpace(options.Topic) || options.Topic.Length > 32)
{
- var gcmApiKey = options["gcmAPIKey"] as string;
-
- currentGcmApiKey = gcmApiKey ?? throw new ArgumentException("options.gcmAPIKey must be of type string");
+ throw new ArgumentException("options.topic must be of type string and not empty and use a maximum of 32 characters from the URL or filename-safe Base64 characters set", nameof(options));
}
-
- if (options.ContainsKey("vapidDetails"))
- {
- var vapidDetails = options["vapidDetails"] as VapidDetails;
- currentVapidDetails = vapidDetails ?? throw new ArgumentException("options.vapidDetails must be of type VapidDetails");
- }
-
- if (options.ContainsKey("TTL"))
+ if (!RegexVariableName().IsMatch(options.Topic))
{
- var ttl = options["TTL"] as int?;
- if (ttl == null)
- {
- throw new ArgumentException("options.TTL must be of type int");
- }
-
- //at this stage ttl cannot be null.
- timeToLive = (int)ttl;
+ throw new ArgumentException("options.topic uses unsupported characters set, use the URL or filename-safe Base64 characters set", nameof(options));
}
}
+ }
- string cryptoKeyHeader = null;
- request.Headers.Add("TTL", timeToLive.ToString());
+ string? cryptoKeyHeader = null;
+ request.Headers.Add("TTL", (options?.TTL ?? WebPushOptions.DefaultTtl).ToString(CultureInfo.InvariantCulture));
+ if (options?.Topic is not null)
+ {
+ request.Headers.Add("Topic", options.Topic);
+ }
+ if (options?.Urgency is not null)
+ {
+ request.Headers.Add("Urgency", options.Urgency.Value.ToKebabCaseLower());
+ }
- foreach (var header in extraHeaders)
+ if (options?.ExtraHeaders is not null)
+ {
+ foreach (var header in options.ExtraHeaders)
{
request.Headers.Add(header.Key, header.Value.ToString());
}
+ }
- 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");
- }
-
- var encryptedPayload = EncryptPayload(subscription, payload);
-
- request.Content = new ByteArrayContent(encryptedPayload.Payload);
- request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
- request.Content.Headers.ContentLength = encryptedPayload.Payload.Length;
- request.Content.Headers.ContentEncoding.Add("aesgcm");
- request.Headers.Add("Encryption", "salt=" + encryptedPayload.Base64EncodeSalt());
- cryptoKeyHeader = @"dh=" + encryptedPayload.Base64EncodePublicKey();
- }
- else
+ var contentEncoding = options?.ContentEncoding ?? WebPushOptions.DefaultContentEncoding;
+ if (!string.IsNullOrEmpty(payload))
+ {
+ if (string.IsNullOrEmpty(subscription.P256DH) || string.IsNullOrEmpty(subscription.Auth))
{
- request.Content = new ByteArrayContent(new byte[0]);
- request.Content.Headers.ContentLength = 0;
+ 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 isGcm = subscription.Endpoint.StartsWith(@"https://android.googleapis.com/gcm/send");
- var isFcm = subscription.Endpoint.StartsWith(@"https://fcm.googleapis.com/fcm/send/");
+ var encryptedPayload = EncryptPayload(subscription, payload);
- if (isGcm)
+ request.Content = new ByteArrayContent(encryptedPayload.Payload);
+ request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
+ request.Content.Headers.ContentLength = encryptedPayload.Payload.Length;
+ request.Content.Headers.ContentEncoding.Add(contentEncoding.ToKebabCaseLower());
+ if (contentEncoding == ContentEncoding.Aesgcm)
{
- if (!string.IsNullOrEmpty(currentGcmApiKey))
- {
- request.Headers.TryAddWithoutValidation("Authorization", "key=" + currentGcmApiKey);
- }
+ request.Headers.Add("Encryption", "salt=" + encryptedPayload.Base64EncodeSalt());
}
- else if (currentVapidDetails != null)
- {
- var uri = new Uri(subscription.Endpoint);
- var audience = uri.Scheme + @"://" + uri.Host;
+ cryptoKeyHeader = @"dh=" + encryptedPayload.Base64EncodePublicKey();
+ }
+ else
+ {
+ request.Content = new ByteArrayContent([]);
+ request.Content.Headers.ContentLength = 0;
+ }
- var vapidHeaders = VapidHelper.GetVapidHeaders(audience, currentVapidDetails.Subject,
- currentVapidDetails.PublicKey, currentVapidDetails.PrivateKey, currentVapidDetails.Expiration);
- request.Headers.Add(@"Authorization", vapidHeaders["Authorization"]);
+ var vapidDetails = options?.VapidDetails ?? _vapidDetails;
+ if (vapidDetails is not null)
+ {
+ 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);
+ request.Headers.Add(@"Authorization", vapidHeaders["Authorization"]);
+ if (contentEncoding == ContentEncoding.Aesgcm)
+ {
if (string.IsNullOrEmpty(cryptoKeyHeader))
{
cryptoKeyHeader = vapidHeaders["Crypto-Key"];
@@ -245,179 +211,212 @@ public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription,
cryptoKeyHeader += @";" + vapidHeaders["Crypto-Key"];
}
}
- else if (isFcm && !string.IsNullOrEmpty(currentGcmApiKey))
- {
- request.Headers.TryAddWithoutValidation("Authorization", "key=" + currentGcmApiKey);
- }
-
+ }
+ if (contentEncoding == ContentEncoding.Aesgcm)
+ {
request.Headers.Add("Crypto-Key", cryptoKeyHeader);
- return request;
}
+ return request;
+ }
- private static EncryptionResult EncryptPayload(PushSubscription subscription, string payload)
+ private static EncryptionResult EncryptPayload(PushSubscription subscription, string payload)
+ {
+ try
+ {
+ return Encryptor.Encrypt(subscription.P256DH, subscription.Auth, payload);
+ }
+ catch (Exception ex)
{
- try
+ if (ex is FormatException || ex is ArgumentException || ex is CryptographicException)
{
- return Encryptor.Encrypt(subscription.P256DH, subscription.Auth, payload);
+ throw new InvalidEncryptionDetailsException("Unable to encrypt the payload with the encryption key of this subscription.", subscription);
}
- catch (Exception ex)
- {
- if (ex is FormatException || ex is ArgumentException)
- {
- throw new InvalidEncryptionDetailsException("Unable to encrypt the payload with the encryption key of this subscription.", subscription);
- }
- throw;
- }
+ throw;
}
+ }
- ///
- /// To send a push notification call this method with a subscription, optional payload and any options
- /// Will exception if unsuccessful
- ///
- /// The PushSubscription you wish to send the notification to.
- /// The payload you wish to send to the user
- ///
- /// Options for the GCM API key and vapid keys can be passed in if they are unique for each
- /// notification.
- ///
- public void SendNotification(PushSubscription subscription, string payload = null,
- Dictionary options = null)
- {
- SendNotificationAsync(subscription, payload, options).ConfigureAwait(false).GetAwaiter().GetResult();
- }
+ ///
+ /// To send a push notification call this method with a subscription, optional payload and any options
+ /// Will exception if unsuccessful
+ ///
+ /// The PushSubscription you wish to send the notification to.
+ /// The payload you wish to send to the user
+ ///
+ /// Options for the web push request, including vapid keys, if they are unique for each
+ /// notification.
+ ///
+ public void SendNotification(PushSubscription subscription, string? payload = null, WebPushOptions? options = null)
+ {
+ SendNotificationAsync(subscription, payload, options).ConfigureAwait(false).GetAwaiter().GetResult();
+ }
- ///
- /// To send a push notification call this method with a subscription, optional payload and any options
- /// Will exception if unsuccessful
- ///
- /// The PushSubscription you wish to send the notification to.
- /// The payload you wish to send to the user
- /// The vapid details for the notification.
- public void SendNotification(PushSubscription subscription, string payload, VapidDetails vapidDetails)
- {
- var options = new Dictionary { ["vapidDetails"] = vapidDetails };
- SendNotification(subscription, payload, options);
- }
+ ///
+ /// To send a push notification call this method with a subscription, optional payload and any options
+ /// Will exception if unsuccessful
+ ///
+ /// The PushSubscription you wish to send the notification to.
+ /// The payload you wish to send to the user
+ ///
+ /// Options for vapid keys can be passed in if they are unique for each
+ /// notification.
+ ///
+ public void SendNotification(PushSubscription subscription, string? payload = null,
+ Dictionary? options = null)
+ {
+ SendNotificationAsync(subscription, payload, options).ConfigureAwait(false).GetAwaiter().GetResult();
+ }
- ///
- /// To send a push notification call this method with a subscription, optional payload and any options
- /// Will exception if unsuccessful
- ///
- /// The PushSubscription you wish to send the notification to.
- /// The payload you wish to send to the user
- /// The GCM API key
- public void SendNotification(PushSubscription subscription, string payload, string gcmApiKey)
- {
- var options = new Dictionary { ["gcmAPIKey"] = gcmApiKey };
- SendNotification(subscription, payload, options);
- }
+ ///
+ /// To send a push notification call this method with a subscription, optional payload and any options
+ /// Will exception if unsuccessful
+ ///
+ /// The PushSubscription you wish to send the notification to.
+ /// The payload you wish to send to the user
+ /// The vapid details for the notification.
+ public void SendNotification(PushSubscription subscription, string payload, VapidDetails vapidDetails)
+ {
+ var options = new WebPushOptions { VapidDetails = vapidDetails, };
+ SendNotification(subscription, payload, options);
+ }
+ ///
+ /// To send a push notification asynchronous call this method with a subscription, optional payload and any options
+ /// Will exception if unsuccessful
+ ///
+ /// The PushSubscription you wish to send the notification to.
+ /// The payload you wish to send to the user
+ ///
+ /// Options for the web push request, including vapid keys, if they are unique for each
+ /// notification.
+ ///
+ /// The cancellation token to cancel operation.
+ public async Task SendNotificationAsync(PushSubscription subscription, string? payload = null, WebPushOptions? options = null, CancellationToken cancellationToken = default)
+ {
+ var request = GenerateRequestDetails(subscription, payload, options);
+ var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
- ///
- /// To send a push notification asynchronous call this method with a subscription, optional payload and any options
- /// Will exception if unsuccessful
- ///
- /// The PushSubscription you wish to send the notification to.
- /// The payload you wish to send to the user
- ///
- /// Options for the GCM API key and vapid keys can be passed in if they are unique for each
- /// notification.
- ///
- /// The cancellation token to cancel operation.
- public async Task SendNotificationAsync(PushSubscription subscription, string payload = null,
- Dictionary options = null, CancellationToken cancellationToken = default)
- {
- var request = GenerateRequestDetails(subscription, payload, options);
- var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ await HandleResponse(response, subscription).ConfigureAwait(false);
+ }
- await HandleResponse(response, subscription).ConfigureAwait(false);
- }
+ ///
+ /// To send a push notification asynchronous call this method with a subscription, optional payload and any options
+ /// Will exception if unsuccessful
+ ///
+ /// The PushSubscription you wish to send the notification to.
+ /// The payload you wish to send to the user
+ ///
+ /// Options for vapid keys can be passed in if they are unique for each
+ /// notification.
+ ///
+ /// The cancellation token to cancel operation.
+ public async Task SendNotificationAsync(PushSubscription subscription, string? payload = null,
+ Dictionary? options = null, CancellationToken cancellationToken = default)
+ {
+ var request = GenerateRequestDetails(subscription, payload, options);
+ var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+
+ await HandleResponse(response, subscription).ConfigureAwait(false);
+ }
- ///
- /// To send a push notification asynchronous call this method with a subscription, optional payload and any options
- /// Will exception if unsuccessful
- ///
- /// The PushSubscription you wish to send the notification to.
- /// The payload you wish to send to the user
- /// The vapid details for the notification.
- ///
- public async Task SendNotificationAsync(PushSubscription subscription, string payload,
- VapidDetails vapidDetails, CancellationToken cancellationToken = default)
+ ///
+ /// To send a push notification asynchronous call this method with a subscription, optional payload and any options
+ /// Will exception if unsuccessful
+ ///
+ /// The PushSubscription you wish to send the notification to.
+ /// The payload you wish to send to the user
+ /// The vapid details for the notification.
+ ///
+ public async Task SendNotificationAsync(PushSubscription subscription, string payload,
+ VapidDetails vapidDetails, CancellationToken cancellationToken = default)
+ {
+ var options = new WebPushOptions { VapidDetails = vapidDetails, };
+ await SendNotificationAsync(subscription, payload, options, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Handle Web Push responses.
+ ///
+ ///
+ ///
+ private static async Task HandleResponse(HttpResponseMessage response, PushSubscription subscription)
+ {
+ // Successful
+ if (response.IsSuccessStatusCode)
{
- var options = new Dictionary { ["vapidDetails"] = vapidDetails };
- await SendNotificationAsync(subscription, payload, options, cancellationToken).ConfigureAwait(false);
+ return;
}
- ///
- /// To send a push notification asynchronous call this method with a subscription, optional payload and any options
- /// Will exception if unsuccessful
- ///
- /// The PushSubscription you wish to send the notification to.
- /// The payload you wish to send to the user
- /// The GCM API key
- ///
- public async Task SendNotificationAsync(PushSubscription subscription, string payload, string gcmApiKey, CancellationToken cancellationToken = default)
+ // Error
+ var responseCodeMessage = $"Received unexpected response code: {(int)response.StatusCode}";
+ switch (response.StatusCode)
{
- var options = new Dictionary { ["gcmAPIKey"] = gcmApiKey };
- await SendNotificationAsync(subscription, payload, options, cancellationToken).ConfigureAwait(false);
+ case HttpStatusCode.BadRequest:
+ responseCodeMessage = "Bad Request";
+ break;
+
+ case HttpStatusCode.RequestEntityTooLarge:
+ responseCodeMessage = "Payload too large";
+ break;
+
+ case (HttpStatusCode)429:
+ responseCodeMessage = "Too many request";
+ break;
+
+ case HttpStatusCode.NotFound:
+ case HttpStatusCode.Gone:
+ responseCodeMessage = "Subscription no longer valid";
+ break;
}
- ///
- /// Handle Web Push responses.
- ///
- ///
- ///
- private static async Task HandleResponse(HttpResponseMessage response, PushSubscription subscription)
+ string? details = null;
+ if (response.Content != null)
{
- // Successful
- if (response.IsSuccessStatusCode)
- {
- return;
- }
+ details = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+ }
- // Error
- var responseCodeMessage = @"Received unexpected response code: " + (int)response.StatusCode;
- switch (response.StatusCode)
- {
- case HttpStatusCode.BadRequest:
- responseCodeMessage = "Bad Request";
- break;
+ var message = string.IsNullOrEmpty(details)
+ ? responseCodeMessage
+ : $"{responseCodeMessage}. Details: {details}";
- case HttpStatusCode.RequestEntityTooLarge:
- responseCodeMessage = "Payload too large";
- break;
+ throw new WebPushException(message, subscription, response);
+ }
- case (HttpStatusCode)429:
- responseCodeMessage = "Too many request";
+ private static WebPushOptions ConvertOptions(Dictionary? options = null)
+ {
+ var wpo = new WebPushOptions();
+ foreach (var option in options ?? [])
+ {
+ switch (option.Key)
+ {
+ case "TTL":
+ var ttl = option.Value as int? ?? throw new ArgumentException("options.TTL must be of type int");
+ wpo.TTL = ttl;
break;
-
- case HttpStatusCode.NotFound:
- case HttpStatusCode.Gone:
- responseCodeMessage = "Subscription no longer valid";
+ case "vapidDetails":
+ var vapids = option.Value as VapidDetails ?? throw new ArgumentException("options.vapidDetails must be of type VapidDetails");
+ wpo.VapidDetails = vapids;
break;
+ case "headers":
+ var headers = option.Value as Dictionary ?? throw new ArgumentException("options.headers must be of type Dictionary");
+ wpo.ExtraHeaders = headers;
+ break;
+ default:
+ throw new ArgumentException($"{option.Key} is an invalid options");
}
-
- string details = null;
- if (response.Content != null)
- {
- details = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
- }
-
- var message = string.IsNullOrEmpty(details)
- ? responseCodeMessage
- : $"{responseCodeMessage}. Details: {details}";
-
- throw new WebPushException(message, subscription, response);
}
+ return wpo;
+ }
- public void Dispose()
+ public void Dispose()
+ {
+ if (_httpClient != null && _isHttpClientInternallyCreated)
{
- if (_httpClient != null && _isHttpClientInternallyCreated)
- {
- _httpClient.Dispose();
- _httpClient = null;
- }
+ _httpClient.Dispose();
+ _httpClient = null;
}
}
+
+ [GeneratedRegex(@"^[A-Za-z0-9\-_]+$", RegexOptions.None, matchTimeoutMilliseconds: 100)]
+ private static partial Regex RegexVariableName();
}
\ No newline at end of file
diff --git a/WebPush/packages.lock.json b/WebPush/packages.lock.json
index 44fe018..4c08e8e 100644
--- a/WebPush/packages.lock.json
+++ b/WebPush/packages.lock.json
@@ -1,55 +1,41 @@
{
"version": 1,
"dependencies": {
- ".NETFramework,Version=v4.8": {
- "Microsoft.NETFramework.ReferenceAssemblies": {
+ "net10.0": {
+ "Microsoft.IdentityModel.JsonWebTokens": {
"type": "Direct",
- "requested": "[1.0.3, )",
- "resolved": "1.0.3",
- "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
+ "requested": "[8.18.0, )",
+ "resolved": "8.18.0",
+ "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==",
"dependencies": {
- "Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3"
+ "Microsoft.IdentityModel.Tokens": "8.18.0"
}
},
- "Microsoft.SourceLink.GitHub": {
+ "Microsoft.IdentityModel.Tokens": {
"type": "Direct",
- "requested": "[10.0.203, )",
- "resolved": "10.0.203",
- "contentHash": "R4Tvr1oACImMS+Y5M7NM07ll9QyJSKnki3Dvz8QwG1W6FEmd+9fmZXAF6BE6UPswHF6n0v41wgMQGlaudOspqA==",
+ "requested": "[8.18.0, )",
+ "resolved": "8.18.0",
+ "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==",
"dependencies": {
- "Microsoft.Build.Tasks.Git": "10.0.203",
- "Microsoft.SourceLink.Common": "10.0.203",
- "System.IO.Hashing": "10.0.7"
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.0",
+ "Microsoft.IdentityModel.Logging": "8.18.0"
}
},
- "Portable.BouncyCastle": {
- "type": "Direct",
- "requested": "[1.9.0, )",
- "resolved": "1.9.0",
- "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw=="
- },
- "System.Text.Json": {
+ "Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.7, )",
"resolved": "10.0.7",
- "contentHash": "F8Pu2QLUMeniVbtiyk7n7LCfFYxlcJ8ASaSwglJyq6dxa34iCQrikQszsgJClIJWuSWjcyhKkV7daAzYJqeVwA==",
- "dependencies": {
- "Microsoft.Bcl.AsyncInterfaces": "10.0.7",
- "System.Buffers": "4.6.1",
- "System.IO.Pipelines": "10.0.7",
- "System.Memory": "4.6.3",
- "System.Runtime.CompilerServices.Unsafe": "6.1.2",
- "System.Text.Encodings.Web": "10.0.7",
- "System.Threading.Tasks.Extensions": "4.6.3",
- "System.ValueTuple": "4.6.2"
- }
+ "contentHash": "AA/yhzFHNtQZXLdqjzujPy25G8EWwGWsAnxOE2zYSBoT/8QHP6ketN3CToD3DFreO653ipUwnKHo22B8AlBMCw=="
},
- "Microsoft.Bcl.AsyncInterfaces": {
- "type": "Transitive",
- "resolved": "10.0.7",
- "contentHash": "g0Xp9A+B8jCf5pNIIhFOQXPJkte3D87shfTLY+ylwfSh22U5oQH6tvvmcUuqJvt/wtwKk0WdNp2OGEczHJlJdg==",
+ "Microsoft.SourceLink.GitHub": {
+ "type": "Direct",
+ "requested": "[10.0.203, )",
+ "resolved": "10.0.203",
+ "contentHash": "R4Tvr1oACImMS+Y5M7NM07ll9QyJSKnki3Dvz8QwG1W6FEmd+9fmZXAF6BE6UPswHF6n0v41wgMQGlaudOspqA==",
"dependencies": {
- "System.Threading.Tasks.Extensions": "4.6.3"
+ "Microsoft.Build.Tasks.Git": "10.0.203",
+ "Microsoft.SourceLink.Common": "10.0.203",
+ "System.IO.Hashing": "10.0.7"
}
},
"Microsoft.Build.Tasks.Git": {
@@ -60,85 +46,69 @@
"System.IO.Hashing": "10.0.7"
}
},
- "Microsoft.NETFramework.ReferenceAssemblies.net48": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
- "resolved": "1.0.3",
- "contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ=="
+ "resolved": "8.0.0",
+ "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg=="
},
- "Microsoft.SourceLink.Common": {
+ "Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.203",
- "contentHash": "QYAnhBCOkT3ZUT/fHag11+bamwlbZ3U9Vi/WfKrD9emdUf1t3aqjWv0V2KtEGHSRSC81aBc8Oy/mvyGpEYd9Pg=="
- },
- "System.Buffers": {
- "type": "Transitive",
- "resolved": "4.6.1",
- "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw=="
- },
- "System.IO.Hashing": {
- "type": "Transitive",
- "resolved": "10.0.7",
- "contentHash": "6hsjdSr4VOXSOnhALkYplHpAxnTG1J33YN42IB6nH2fEg4QnJqrZ4Ft+qn7mkrKAOYC8pCSFYwVWw6rQbmwgLQ==",
+ "resolved": "8.0.0",
+ "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
"dependencies": {
- "System.Buffers": "4.6.1",
- "System.Memory": "4.6.3"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
},
- "System.IO.Pipelines": {
+ "Microsoft.IdentityModel.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.7",
- "contentHash": "LTxXYYKmRhPKWveYmfzuRTUnzsfY7CN+WOq6aTRgYE9vJ8BUvIWPCaSx4HxqBwXViTPSjR9cHDOVuVPuZGRR/Q==",
- "dependencies": {
- "System.Buffers": "4.6.1",
- "System.Memory": "4.6.3",
- "System.Threading.Tasks.Extensions": "4.6.3"
- }
+ "resolved": "8.18.0",
+ "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ=="
},
- "System.Memory": {
+ "Microsoft.IdentityModel.Logging": {
"type": "Transitive",
- "resolved": "4.6.3",
- "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==",
+ "resolved": "8.18.0",
+ "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==",
"dependencies": {
- "System.Buffers": "4.6.1",
- "System.Numerics.Vectors": "4.6.1",
- "System.Runtime.CompilerServices.Unsafe": "6.1.2"
+ "Microsoft.IdentityModel.Abstractions": "8.18.0"
}
},
- "System.Numerics.Vectors": {
- "type": "Transitive",
- "resolved": "4.6.1",
- "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q=="
- },
- "System.Runtime.CompilerServices.Unsafe": {
+ "Microsoft.SourceLink.Common": {
"type": "Transitive",
- "resolved": "6.1.2",
- "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw=="
+ "resolved": "10.0.203",
+ "contentHash": "QYAnhBCOkT3ZUT/fHag11+bamwlbZ3U9Vi/WfKrD9emdUf1t3aqjWv0V2KtEGHSRSC81aBc8Oy/mvyGpEYd9Pg=="
},
- "System.Text.Encodings.Web": {
+ "System.IO.Hashing": {
"type": "Transitive",
"resolved": "10.0.7",
- "contentHash": "WUH+viO8VDG8NpFKvOBwpeyKUiPOMz3kQpA6AKCD4b2NG1pBhyC4AwTb357iZmTxZDnkM4IsFnvzN8W8OKmsHg==",
+ "contentHash": "6hsjdSr4VOXSOnhALkYplHpAxnTG1J33YN42IB6nH2fEg4QnJqrZ4Ft+qn7mkrKAOYC8pCSFYwVWw6rQbmwgLQ=="
+ }
+ },
+ "net8.0": {
+ "Microsoft.IdentityModel.JsonWebTokens": {
+ "type": "Direct",
+ "requested": "[8.18.0, )",
+ "resolved": "8.18.0",
+ "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==",
"dependencies": {
- "System.Buffers": "4.6.1",
- "System.Memory": "4.6.3",
- "System.Runtime.CompilerServices.Unsafe": "6.1.2"
+ "Microsoft.IdentityModel.Tokens": "8.18.0"
}
},
- "System.Threading.Tasks.Extensions": {
- "type": "Transitive",
- "resolved": "4.6.3",
- "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==",
+ "Microsoft.IdentityModel.Tokens": {
+ "type": "Direct",
+ "requested": "[8.18.0, )",
+ "resolved": "8.18.0",
+ "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==",
"dependencies": {
- "System.Runtime.CompilerServices.Unsafe": "6.1.2"
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.0",
+ "Microsoft.IdentityModel.Logging": "8.18.0"
}
},
- "System.ValueTuple": {
- "type": "Transitive",
- "resolved": "4.6.2",
- "contentHash": "yQgmjfFximrNm9LIV3mL6T5MzjeC+epeE5rl4hXxAlYmxby7RM1dPSkIKXk9HNkl6G54h2JHOmLD46+Pey+IRg=="
- }
- },
- ".NETStandard,Version=v2.1": {
+ "Microsoft.NET.ILLink.Tasks": {
+ "type": "Direct",
+ "requested": "[8.0.26, )",
+ "resolved": "8.0.26",
+ "contentHash": "o7/yVssM2r9Wyln2s9edBd5ANZXqdSdBI+g7JqXkyJmXrhs2WsJp25K5yPnYrTgdKBCjKB8bg+O2oew4sgzFaA=="
+ },
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[10.0.203, )",
@@ -150,29 +120,6 @@
"System.IO.Hashing": "10.0.7"
}
},
- "Portable.BouncyCastle": {
- "type": "Direct",
- "requested": "[1.9.0, )",
- "resolved": "1.9.0",
- "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw=="
- },
- "System.Text.Json": {
- "type": "Direct",
- "requested": "[10.0.7, )",
- "resolved": "10.0.7",
- "contentHash": "F8Pu2QLUMeniVbtiyk7n7LCfFYxlcJ8ASaSwglJyq6dxa34iCQrikQszsgJClIJWuSWjcyhKkV7daAzYJqeVwA==",
- "dependencies": {
- "Microsoft.Bcl.AsyncInterfaces": "10.0.7",
- "System.IO.Pipelines": "10.0.7",
- "System.Runtime.CompilerServices.Unsafe": "6.1.2",
- "System.Text.Encodings.Web": "10.0.7"
- }
- },
- "Microsoft.Bcl.AsyncInterfaces": {
- "type": "Transitive",
- "resolved": "10.0.7",
- "contentHash": "g0Xp9A+B8jCf5pNIIhFOQXPJkte3D87shfTLY+ylwfSh22U5oQH6tvvmcUuqJvt/wtwKk0WdNp2OGEczHJlJdg=="
- },
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "10.0.203",
@@ -181,59 +128,30 @@
"System.IO.Hashing": "10.0.7"
}
},
- "Microsoft.SourceLink.Common": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.203",
- "contentHash": "QYAnhBCOkT3ZUT/fHag11+bamwlbZ3U9Vi/WfKrD9emdUf1t3aqjWv0V2KtEGHSRSC81aBc8Oy/mvyGpEYd9Pg=="
+ "resolved": "8.0.0",
+ "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg=="
},
- "System.IO.Hashing": {
+ "Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.7",
- "contentHash": "6hsjdSr4VOXSOnhALkYplHpAxnTG1J33YN42IB6nH2fEg4QnJqrZ4Ft+qn7mkrKAOYC8pCSFYwVWw6rQbmwgLQ=="
- },
- "System.IO.Pipelines": {
- "type": "Transitive",
- "resolved": "10.0.7",
- "contentHash": "LTxXYYKmRhPKWveYmfzuRTUnzsfY7CN+WOq6aTRgYE9vJ8BUvIWPCaSx4HxqBwXViTPSjR9cHDOVuVPuZGRR/Q=="
- },
- "System.Runtime.CompilerServices.Unsafe": {
- "type": "Transitive",
- "resolved": "6.1.2",
- "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw=="
- },
- "System.Text.Encodings.Web": {
- "type": "Transitive",
- "resolved": "10.0.7",
- "contentHash": "WUH+viO8VDG8NpFKvOBwpeyKUiPOMz3kQpA6AKCD4b2NG1pBhyC4AwTb357iZmTxZDnkM4IsFnvzN8W8OKmsHg==",
+ "resolved": "8.0.0",
+ "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
"dependencies": {
- "System.Runtime.CompilerServices.Unsafe": "6.1.2"
- }
- }
- },
- "net10.0": {
- "Microsoft.SourceLink.GitHub": {
- "type": "Direct",
- "requested": "[10.0.203, )",
- "resolved": "10.0.203",
- "contentHash": "R4Tvr1oACImMS+Y5M7NM07ll9QyJSKnki3Dvz8QwG1W6FEmd+9fmZXAF6BE6UPswHF6n0v41wgMQGlaudOspqA==",
- "dependencies": {
- "Microsoft.Build.Tasks.Git": "10.0.203",
- "Microsoft.SourceLink.Common": "10.0.203",
- "System.IO.Hashing": "10.0.7"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
},
- "Portable.BouncyCastle": {
- "type": "Direct",
- "requested": "[1.9.0, )",
- "resolved": "1.9.0",
- "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw=="
+ "Microsoft.IdentityModel.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.18.0",
+ "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ=="
},
- "Microsoft.Build.Tasks.Git": {
+ "Microsoft.IdentityModel.Logging": {
"type": "Transitive",
- "resolved": "10.0.203",
- "contentHash": "m56WtzvIcL6t7JR3c7ogYitHizNM2QnRSo8yqxrQi+m5E/GGyDEmqymP+2p6YsFXn0j/Tzz67s4FQnrTLC7GKQ==",
+ "resolved": "8.18.0",
+ "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==",
"dependencies": {
- "System.IO.Hashing": "10.0.7"
+ "Microsoft.IdentityModel.Abstractions": "8.18.0"
}
},
"Microsoft.SourceLink.Common": {
@@ -247,7 +165,32 @@
"contentHash": "6hsjdSr4VOXSOnhALkYplHpAxnTG1J33YN42IB6nH2fEg4QnJqrZ4Ft+qn7mkrKAOYC8pCSFYwVWw6rQbmwgLQ=="
}
},
- "net8.0": {
+ "net9.0": {
+ "Microsoft.IdentityModel.JsonWebTokens": {
+ "type": "Direct",
+ "requested": "[8.18.0, )",
+ "resolved": "8.18.0",
+ "contentHash": "ZUMJt3r1zOi67AVSfnh3u9hg9KCq06roOIX5gs7FqsucSZ/VTsI89DI9h2gHyU0xOtj/qVZV2ugWS6JlLMTwHQ==",
+ "dependencies": {
+ "Microsoft.IdentityModel.Tokens": "8.18.0"
+ }
+ },
+ "Microsoft.IdentityModel.Tokens": {
+ "type": "Direct",
+ "requested": "[8.18.0, )",
+ "resolved": "8.18.0",
+ "contentHash": "c6ksXXFj5oPPsl8pfsui5zv8Gs7uxrGetXCTc1p7k7Nue/C8iBMtAVgtRrH7Esqe596QWD7KS3exKYY1FJG2iw==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.0",
+ "Microsoft.IdentityModel.Logging": "8.18.0"
+ }
+ },
+ "Microsoft.NET.ILLink.Tasks": {
+ "type": "Direct",
+ "requested": "[9.0.15, )",
+ "resolved": "9.0.15",
+ "contentHash": "EejcbfCMR77Dthy77qxRbEShmzLApHZUPqXMBVQK+A0pNrRThkaHoGGMGvbq/gTkC/waKcDEgjBkbaejB58Wtw=="
+ },
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[10.0.203, )",
@@ -259,12 +202,6 @@
"System.IO.Hashing": "10.0.7"
}
},
- "Portable.BouncyCastle": {
- "type": "Direct",
- "requested": "[1.9.0, )",
- "resolved": "1.9.0",
- "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw=="
- },
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "10.0.203",
@@ -273,41 +210,30 @@
"System.IO.Hashing": "10.0.7"
}
},
- "Microsoft.SourceLink.Common": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.203",
- "contentHash": "QYAnhBCOkT3ZUT/fHag11+bamwlbZ3U9Vi/WfKrD9emdUf1t3aqjWv0V2KtEGHSRSC81aBc8Oy/mvyGpEYd9Pg=="
+ "resolved": "8.0.0",
+ "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg=="
},
- "System.IO.Hashing": {
+ "Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
- "resolved": "10.0.7",
- "contentHash": "6hsjdSr4VOXSOnhALkYplHpAxnTG1J33YN42IB6nH2fEg4QnJqrZ4Ft+qn7mkrKAOYC8pCSFYwVWw6rQbmwgLQ=="
- }
- },
- "net9.0": {
- "Microsoft.SourceLink.GitHub": {
- "type": "Direct",
- "requested": "[10.0.203, )",
- "resolved": "10.0.203",
- "contentHash": "R4Tvr1oACImMS+Y5M7NM07ll9QyJSKnki3Dvz8QwG1W6FEmd+9fmZXAF6BE6UPswHF6n0v41wgMQGlaudOspqA==",
+ "resolved": "8.0.0",
+ "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
"dependencies": {
- "Microsoft.Build.Tasks.Git": "10.0.203",
- "Microsoft.SourceLink.Common": "10.0.203",
- "System.IO.Hashing": "10.0.7"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
},
- "Portable.BouncyCastle": {
- "type": "Direct",
- "requested": "[1.9.0, )",
- "resolved": "1.9.0",
- "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw=="
+ "Microsoft.IdentityModel.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.18.0",
+ "contentHash": "8VUcDy66uw1GUC/ytyRJAUgGxydPu2rLtUbUAiniCHd5SMB/01Q28XgqFyxIqb3srz6HWTgSsZdDbkdVJr3LXQ=="
},
- "Microsoft.Build.Tasks.Git": {
+ "Microsoft.IdentityModel.Logging": {
"type": "Transitive",
- "resolved": "10.0.203",
- "contentHash": "m56WtzvIcL6t7JR3c7ogYitHizNM2QnRSo8yqxrQi+m5E/GGyDEmqymP+2p6YsFXn0j/Tzz67s4FQnrTLC7GKQ==",
+ "resolved": "8.18.0",
+ "contentHash": "c2l/VEtW1XI/ifcu49xzDwgrZZ0a0aX/TwCPC7mEHFQk/KixDgtSdjB5eDhYyCO38GJiRUjeRTz9aWCy1t55ww==",
"dependencies": {
- "System.IO.Hashing": "10.0.7"
+ "Microsoft.IdentityModel.Abstractions": "8.18.0"
}
},
"Microsoft.SourceLink.Common": {