From 208ddf314100922ef2b5421227f1c69e86b9583f Mon Sep 17 00:00:00 2001 From: "Anil.Senel" Date: Tue, 15 Aug 2023 09:10:28 +0300 Subject: [PATCH 1/6] Vapid --- .../AdsPush.Abstraction.csproj | 1 + src/AdsPush.Abstraction/AdsPushProvider.cs | 7 +- src/AdsPush.Abstraction/AdsPushTarget.cs | 4 + .../Settings/AdsPushAppSettings.cs | 13 +- .../Settings/AdsPushVapidSettings.cs | 25 +++ src/AdsPush.Abstraction/Vapid/VapidError.cs | 33 ++++ .../Vapid/VapidErrorReasonCode.cs | 30 +++ src/AdsPush.Abstraction/Vapid/VapidRequest.cs | 82 ++++++++ .../Vapid/VapidRequestActionAction.cs | 20 ++ .../Vapid/VapidResponse.cs | 32 ++++ src/AdsPush.Vapid/AdsPush.Vapid.csproj | 37 ++++ .../Extensions/BuilderExtensions.cs | 39 ++++ .../Extensions/MappingExtensions.cs | 60 ++++++ .../IVapidPushNotificationSender.cs | 51 +++++ .../IVapidPushNotificationSenderFactory.cs | 15 ++ .../Settings/VapidSettingsSection.cs | 9 + src/AdsPush.Vapid/Util/ECKeyHelper.cs | 66 +++++++ src/AdsPush.Vapid/Util/EncryptionResult.cs | 21 ++ src/AdsPush.Vapid/Util/Encryptor.cs | 177 +++++++++++++++++ src/AdsPush.Vapid/Util/JwsSigner.cs | 79 ++++++++ src/AdsPush.Vapid/Util/UrlBase64.cs | 34 ++++ src/AdsPush.Vapid/VapidHelper.cs | 180 ++++++++++++++++++ .../VapidPushNotificationSender.cs | 180 ++++++++++++++++++ .../VapidPushNotificationSenderFactory.cs | 56 ++++++ src/AdsPush.Vapid/VapidSubscription.cs | 53 ++++++ src/AdsPush/AdsPush.csproj | 1 + src/AdsPush/Extensions/BuilderExtension.cs | 6 +- 27 files changed, 1305 insertions(+), 6 deletions(-) create mode 100644 src/AdsPush.Abstraction/Settings/AdsPushVapidSettings.cs create mode 100644 src/AdsPush.Abstraction/Vapid/VapidError.cs create mode 100644 src/AdsPush.Abstraction/Vapid/VapidErrorReasonCode.cs create mode 100644 src/AdsPush.Abstraction/Vapid/VapidRequest.cs create mode 100644 src/AdsPush.Abstraction/Vapid/VapidRequestActionAction.cs create mode 100644 src/AdsPush.Abstraction/Vapid/VapidResponse.cs create mode 100644 src/AdsPush.Vapid/AdsPush.Vapid.csproj create mode 100644 src/AdsPush.Vapid/Extensions/BuilderExtensions.cs create mode 100644 src/AdsPush.Vapid/Extensions/MappingExtensions.cs create mode 100644 src/AdsPush.Vapid/IVapidPushNotificationSender.cs create mode 100644 src/AdsPush.Vapid/IVapidPushNotificationSenderFactory.cs create mode 100644 src/AdsPush.Vapid/Settings/VapidSettingsSection.cs create mode 100644 src/AdsPush.Vapid/Util/ECKeyHelper.cs create mode 100644 src/AdsPush.Vapid/Util/EncryptionResult.cs create mode 100644 src/AdsPush.Vapid/Util/Encryptor.cs create mode 100644 src/AdsPush.Vapid/Util/JwsSigner.cs create mode 100644 src/AdsPush.Vapid/Util/UrlBase64.cs create mode 100644 src/AdsPush.Vapid/VapidHelper.cs create mode 100644 src/AdsPush.Vapid/VapidPushNotificationSender.cs create mode 100644 src/AdsPush.Vapid/VapidPushNotificationSenderFactory.cs create mode 100644 src/AdsPush.Vapid/VapidSubscription.cs diff --git a/src/AdsPush.Abstraction/AdsPush.Abstraction.csproj b/src/AdsPush.Abstraction/AdsPush.Abstraction.csproj index 689d07c..e86c066 100644 --- a/src/AdsPush.Abstraction/AdsPush.Abstraction.csproj +++ b/src/AdsPush.Abstraction/AdsPush.Abstraction.csproj @@ -25,4 +25,5 @@ + diff --git a/src/AdsPush.Abstraction/AdsPushProvider.cs b/src/AdsPush.Abstraction/AdsPushProvider.cs index 5cf5378..e0369eb 100644 --- a/src/AdsPush.Abstraction/AdsPushProvider.cs +++ b/src/AdsPush.Abstraction/AdsPushProvider.cs @@ -12,6 +12,11 @@ public enum AdsPushProvider /// /// FCM - Firebase Cloud Messaging. /// - Firebase + Firebase, + + /// + /// Web Push + /// + VapidWebPush, } } diff --git a/src/AdsPush.Abstraction/AdsPushTarget.cs b/src/AdsPush.Abstraction/AdsPushTarget.cs index 2da8354..9a756aa 100644 --- a/src/AdsPush.Abstraction/AdsPushTarget.cs +++ b/src/AdsPush.Abstraction/AdsPushTarget.cs @@ -13,5 +13,9 @@ public enum AdsPushTarget /// Android /// Android, + /// + /// Mobile & PC Browsers, progressive web application (PWA) + /// + BrowserAndPwa } } diff --git a/src/AdsPush.Abstraction/Settings/AdsPushAppSettings.cs b/src/AdsPush.Abstraction/Settings/AdsPushAppSettings.cs index d8f75b9..1a6a571 100644 --- a/src/AdsPush.Abstraction/Settings/AdsPushAppSettings.cs +++ b/src/AdsPush.Abstraction/Settings/AdsPushAppSettings.cs @@ -8,26 +8,31 @@ namespace AdsPush.Abstraction.Settings public class AdsPushAppSettings { /// - /// + /// /// public AdsPushAppSettings() { this.TargetMappings = new Dictionary(); } - + /// /// Mapping for platform and target service for that platform. /// public Dictionary TargetMappings { get; set; } - + /// /// Firebase configuration. /// public AdsPushFirebaseSettings Firebase { get; set; } - + /// /// APNS Configuration. /// public AdsPushAPNSSettings Apns { get; set; } + + /// + /// Vapid configuration + /// + public AdsPushVapidSettings Vapid { get; set; } } } diff --git a/src/AdsPush.Abstraction/Settings/AdsPushVapidSettings.cs b/src/AdsPush.Abstraction/Settings/AdsPushVapidSettings.cs new file mode 100644 index 0000000..8874856 --- /dev/null +++ b/src/AdsPush.Abstraction/Settings/AdsPushVapidSettings.cs @@ -0,0 +1,25 @@ +namespace AdsPush.Abstraction.Settings +{ + public class AdsPushVapidSettings + { + /// + /// Gets or sets the public key for VAPID authentication. This should be a URL-safe base64 encoded string. + /// + public string PublicKey { get; set; } + + /// + /// Gets or sets the private key for VAPID authentication. This should be a URL-safe base64 encoded string. + /// + public string PrivateKey { get; set; } + + /// + /// Gets or sets the subject for VAPID authentication. This should be a mailto or a URL. + /// + public string Subject { get; set; } + + /// + /// Gets or sets the Time To Live (TTL) for the notification. This defines how long a push message is retained if the user's device is offline. If not delivered in this time, the message will be dropped. + /// + public long? TTL { get; set; } + } +} diff --git a/src/AdsPush.Abstraction/Vapid/VapidError.cs b/src/AdsPush.Abstraction/Vapid/VapidError.cs new file mode 100644 index 0000000..6ca79eb --- /dev/null +++ b/src/AdsPush.Abstraction/Vapid/VapidError.cs @@ -0,0 +1,33 @@ +using System.Net.Http; + +namespace AdsPush.Abstraction.Vapid +{ + /// + /// Vapid error. + /// + public class VapidError + { + public VapidError() + { + } + + public VapidError( + VapidErrorReasonCode reasonCode, + HttpResponseMessage httpResponse) + { + this.ReasonCode = reasonCode; + this.HttpResponse = httpResponse; + } + + /// + /// Vapid error reason. + /// + public VapidErrorReasonCode ReasonCode { get; set; } + + /// + /// APNS Response. + /// + /// + public HttpResponseMessage HttpResponse { get; set; } + } +} diff --git a/src/AdsPush.Abstraction/Vapid/VapidErrorReasonCode.cs b/src/AdsPush.Abstraction/Vapid/VapidErrorReasonCode.cs new file mode 100644 index 0000000..1fec8f2 --- /dev/null +++ b/src/AdsPush.Abstraction/Vapid/VapidErrorReasonCode.cs @@ -0,0 +1,30 @@ +namespace AdsPush.Abstraction.Vapid +{ + public enum VapidErrorReasonCode + { + /// + /// Unknown error, possibly due to an unexpected scenario. + /// + UnknownError = 0, + + /// + /// Push token is invalid or expired. + /// + InvalidToken = 1, + + /// + /// The push notification service is unavailable or unreachable. + /// + ServiceUnavailable = 2, + + /// + /// One or more of the provided arguments is invalid or missing. + /// + InvalidArgument = 3, + + /// + /// The authentication configuration is missing or incorrect. + /// + InvalidAuthConfiguration = 4, + } +} diff --git a/src/AdsPush.Abstraction/Vapid/VapidRequest.cs b/src/AdsPush.Abstraction/Vapid/VapidRequest.cs new file mode 100644 index 0000000..6dd1b4c --- /dev/null +++ b/src/AdsPush.Abstraction/Vapid/VapidRequest.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; + +namespace AdsPush.Abstraction.Vapid +{ + public class VapidRequest + { + /// + /// The title of the notification. + /// + public string Title { get; set; } + + /// + /// The message body of the notification. + /// + public string Message { get; set; } + + /// + /// URL of the image to be displayed in the notification. + /// + public string Image { get; set; } + + /// + /// A string that categorizes a notification. + /// + public string Tag { get; set; } + + /// + /// The URL of the badge to be displayed in the notification. + /// + public string Badge { get; set; } + + /// + /// The URL of the icon to be displayed in the notification. + /// + public string Icon { get; set; } + + /// + /// The sound to play when the notification is displayed. + /// + public string Sound { get; set; } + + /// + /// URL to be opened when the notification is clicked. + /// + public string ClickAction { get; set; } + + /// + /// Time to live for the notification, in seconds. Defines how long a push message is retained if the user's device is offline. If not delivered in this time, the message will be dropped. + /// + public int? TTL { get; set; } + + /// + /// Indicates whether the notification requires user interaction. + /// + public bool RequireInteraction { get; set; } + + /// + /// Pattern for the vibration (if supported). + /// + public string VibratePattern { get; set; } + + /// + /// List of actions to be displayed in the notification. + /// + public List Actions { get; set; } + + /// + /// Custom data payload for the notification. + /// + public Dictionary Data { get; set; } + + /// + /// List of URL arguments for Safari. + /// + public List UrlArgs { get; set; } + + /// + /// Language code for the notification. + /// + public string Language { get; set; } + } +} diff --git a/src/AdsPush.Abstraction/Vapid/VapidRequestActionAction.cs b/src/AdsPush.Abstraction/Vapid/VapidRequestActionAction.cs new file mode 100644 index 0000000..87d4ab9 --- /dev/null +++ b/src/AdsPush.Abstraction/Vapid/VapidRequestActionAction.cs @@ -0,0 +1,20 @@ +namespace AdsPush.Abstraction.Vapid +{ + public class VapidRequestActionAction + { + /// + /// Identifier for the action. + /// + public string Action { get; set; } + + /// + /// The title for the action button. + /// + public string Title { get; set; } + + /// + /// URL of the icon to be displayed for the action button. + /// + public string Icon { get; set; } + } +} diff --git a/src/AdsPush.Abstraction/Vapid/VapidResponse.cs b/src/AdsPush.Abstraction/Vapid/VapidResponse.cs new file mode 100644 index 0000000..73fe8b6 --- /dev/null +++ b/src/AdsPush.Abstraction/Vapid/VapidResponse.cs @@ -0,0 +1,32 @@ +using AdsPush.Abstraction.APNS; + +namespace AdsPush.Abstraction.Vapid +{ + /// + /// Generic service response + /// + public class VapidResponse + { + public VapidResponse() + { + } + + public VapidResponse( + bool isSuccess, + VapidError error) + { + this.IsSuccess = isSuccess; + this.Error = error; + } + + /// + /// The service response success or not. + /// + public bool IsSuccess { get; set; } + + /// + /// Represents error if occurrences. + /// + public VapidError Error { get; set; } + } +} diff --git a/src/AdsPush.Vapid/AdsPush.Vapid.csproj b/src/AdsPush.Vapid/AdsPush.Vapid.csproj new file mode 100644 index 0000000..c0eccd4 --- /dev/null +++ b/src/AdsPush.Vapid/AdsPush.Vapid.csproj @@ -0,0 +1,37 @@ + + + + netstandard2.0 + + + + + + + + + true + / + LICENSE + + + true + / + logo.png + + + true + / + README-NUGET.md + + + + + + + + + + + + diff --git a/src/AdsPush.Vapid/Extensions/BuilderExtensions.cs b/src/AdsPush.Vapid/Extensions/BuilderExtensions.cs new file mode 100644 index 0000000..e0ef482 --- /dev/null +++ b/src/AdsPush.Vapid/Extensions/BuilderExtensions.cs @@ -0,0 +1,39 @@ +using System; +using System.Net.Http; +using AdsPush.Vapid.Settings; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace AdsPush.Vapid.Extensions +{ + public static class BuilderExtensions + { + + /// + /// Configures to be able to creates instance. + /// + /// + /// + /// + /// + public static IServiceCollection AddVapidNotificationServiceFactory( + this IServiceCollection services, + Action vapidSettingsSectionsAction = null, + HttpClient httpClient = null) + { + var vapidSettingsSection = new VapidSettingsSection(); + vapidSettingsSectionsAction?.Invoke(vapidSettingsSection); + + services.AddSingleton(serviceProvider => + { + vapidSettingsSection = vapidSettingsSectionsAction is null + ? serviceProvider.GetService>()?.Value + : vapidSettingsSection; + + return new VapidPushNotificationSenderFactory(vapidSettingsSection, httpClient ?? new HttpClient()); + }); + + return services; + } + } +} diff --git a/src/AdsPush.Vapid/Extensions/MappingExtensions.cs b/src/AdsPush.Vapid/Extensions/MappingExtensions.cs new file mode 100644 index 0000000..bf3848f --- /dev/null +++ b/src/AdsPush.Vapid/Extensions/MappingExtensions.cs @@ -0,0 +1,60 @@ +using System.Linq; +using AdsPush.Abstraction; +using AdsPush.Abstraction.Vapid; + +namespace AdsPush.Vapid.Extensions +{ + public static class MappingExtensions + { + public static AdsPushException CreateException( + this VapidError error) + { + switch (error.ReasonCode) + { + case VapidErrorReasonCode.UnknownError: + return new AdsPushException( + error.ReasonCode.ToString(), + AdsPushErrorType.Unknown, + error.HttpResponse); + case VapidErrorReasonCode.InvalidToken: + return new AdsPushException( + error.ReasonCode.ToString(), + AdsPushErrorType.InvalidToken, + error.HttpResponse); + case VapidErrorReasonCode.ServiceUnavailable: + return new AdsPushException( + error.ReasonCode.ToString(), + AdsPushErrorType.ServiceUnavailable, + error.HttpResponse); + case VapidErrorReasonCode.InvalidArgument: + return new AdsPushException( + error.ReasonCode.ToString(), + AdsPushErrorType.InvalidArgument, + error.HttpResponse); + case VapidErrorReasonCode.InvalidAuthConfiguration: + return new AdsPushException( + error.ReasonCode.ToString(), + AdsPushErrorType.InvalidAuthConfiguration, + error.HttpResponse); + default: + return new AdsPushException( + error.ReasonCode.ToString(), + AdsPushErrorType.Unknown, + error.HttpResponse); + } + } + + public static VapidRequest CreateRequest( + this AdsPushBasicSendPayload payload) + { + return new VapidRequest() + { + Title = payload.Title.Text, + Message = payload.Detail.Text, + Tag = payload.GroupId, + Sound = payload.Sound, + Data = payload.Parameters.ToDictionary(x=>x.Key, x=>x.Value.ToString()), + }; + } + } +} diff --git a/src/AdsPush.Vapid/IVapidPushNotificationSender.cs b/src/AdsPush.Vapid/IVapidPushNotificationSender.cs new file mode 100644 index 0000000..b959e02 --- /dev/null +++ b/src/AdsPush.Vapid/IVapidPushNotificationSender.cs @@ -0,0 +1,51 @@ +using System.Threading; +using System.Threading.Tasks; +using AdsPush.Abstraction; +using AdsPush.Abstraction.Vapid; + +namespace AdsPush.Vapid +{ + /// + /// Use to commute VAPID Notification supported services. + /// + public interface IVapidPushNotificationSender + { + /// + /// and + /// + /// + /// Use to pass subscription info + /// + /// + /// + Task SendAsync( + VapidSubscription subscription, + string payload, + CancellationToken cancellationToken = default); + + /// + /// and + /// + /// + /// Use to pass subscription info + /// The payload model. + /// + /// + Task SendAsync( + VapidSubscription subscription, + VapidRequest payload, + CancellationToken cancellationToken = default); + + /// + /// + /// + /// + /// + /// + /// + Task SendAsync( + string subscriptionJson, + AdsPushBasicSendPayload payload, + CancellationToken cancellationToken = default); + } +} diff --git a/src/AdsPush.Vapid/IVapidPushNotificationSenderFactory.cs b/src/AdsPush.Vapid/IVapidPushNotificationSenderFactory.cs new file mode 100644 index 0000000..4229e00 --- /dev/null +++ b/src/AdsPush.Vapid/IVapidPushNotificationSenderFactory.cs @@ -0,0 +1,15 @@ +using AdsPush.Abstraction.Settings; +using AdsPush.Vapid.Settings; + +namespace AdsPush.Vapid +{ + public interface IVapidPushNotificationSenderFactory + { + IVapidPushNotificationSender GetSender( + string appName); + + IVapidPushNotificationSender GetSender( + string appName, + AdsPushVapidSettings vapidSettings); + } +} diff --git a/src/AdsPush.Vapid/Settings/VapidSettingsSection.cs b/src/AdsPush.Vapid/Settings/VapidSettingsSection.cs new file mode 100644 index 0000000..76f6a46 --- /dev/null +++ b/src/AdsPush.Vapid/Settings/VapidSettingsSection.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using AdsPush.Abstraction.Settings; + +namespace AdsPush.Vapid.Settings +{ + public class VapidSettingsSection : Dictionary + { + } +} diff --git a/src/AdsPush.Vapid/Util/ECKeyHelper.cs b/src/AdsPush.Vapid/Util/ECKeyHelper.cs new file mode 100644 index 0000000..92f2f6c --- /dev/null +++ b/src/AdsPush.Vapid/Util/ECKeyHelper.cs @@ -0,0 +1,66 @@ +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; + +namespace AdsPush.Vapid.Util +{ + internal static class ECKeyHelper + { + public static ECPrivateKeyParameters GetPrivateKey( + byte[] privateKey) + { + 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----"; + + var reader = new StringReader(pemKey); + var pemReader = new PemReader(reader); + var keyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject(); + + 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); + + Asn1Object derSequence = new DerSequence(keyTypeParameters, derEncodedKey); + + var base64EncodedDerSequence = Convert.ToBase64String(derSequence.GetDerEncoded()); + var pemKey = "-----BEGIN PUBLIC KEY-----\n"; + pemKey += base64EncodedDerSequence; + pemKey += "\n-----END PUBLIC KEY-----"; + + var reader = new StringReader(pemKey); + var pemReader = new PemReader(reader); + var keyPair = pemReader.ReadObject(); + return (ECPublicKeyParameters)keyPair; + } + + public static AsymmetricCipherKeyPair GenerateKeys() + { + 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())); + + return keyPairGenerator.GenerateKeyPair(); + } + } +} diff --git a/src/AdsPush.Vapid/Util/EncryptionResult.cs b/src/AdsPush.Vapid/Util/EncryptionResult.cs new file mode 100644 index 0000000..714af3e --- /dev/null +++ b/src/AdsPush.Vapid/Util/EncryptionResult.cs @@ -0,0 +1,21 @@ +namespace AdsPush.Vapid.Util +{ + // @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 string Base64EncodePublicKey() + { + return UrlBase64.Encode(this.PublicKey); + } + + public string Base64EncodeSalt() + { + return UrlBase64.Encode(this.Salt); + } + } +} diff --git a/src/AdsPush.Vapid/Util/Encryptor.cs b/src/AdsPush.Vapid/Util/Encryptor.cs new file mode 100644 index 0000000..ab11cdb --- /dev/null +++ b/src/AdsPush.Vapid/Util/Encryptor.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Linq; +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 AdsPush.Vapid.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); + } + + private static EncryptionResult Encrypt( + byte[] userKey, + byte[] userSecret, + byte[] payload) + { + var salt = GenerateSalt(16); + var serverKeyPair = ECKeyHelper.GenerateKeys(); + + var ecdhAgreement = AgreementUtilities.GetBasicAgreement("ECDH"); + ecdhAgreement.Init(serverKeyPair.Private); + + var userPublicKey = ECKeyHelper.GetPublicKey(userKey); + + var key = ecdhAgreement.CalculateAgreement(userPublicKey).ToByteArrayUnsigned(); + var serverPublicKey = ((ECPublicKeyParameters)serverKeyPair.Public).Q.GetEncoded(false); + + 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); + + var input = AddPaddingToInput(payload); + var encryptedMessage = EncryptAes(nonce, cek, input); + + return new EncryptionResult + { + Salt = salt, + Payload = encryptedMessage, + PublicKey = serverPublicKey + }; + } + + private static byte[] GenerateSalt( + int length) + { + var salt = new byte[length]; + var random = new Random(); + random.NextBytes(salt); + return salt; + } + + 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; + } + + 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); + + //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); + + //byte[] tag = cipher.GetMac(); + return cipherText; + } + + private 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); + + if (result.Length > length) + { + Array.Resize(ref result, length); + } + + 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) + { + 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(); + } + } + + public class HmacSha256 + { + private readonly HMac _hmac; + + public HmacSha256( + byte[] key) + { + this._hmac = new HMac(new Sha256Digest()); + this._hmac.Init(new KeyParameter(key)); + } + + public byte[] ComputeHash( + byte[] value) + { + var resBuf = new byte[this._hmac.GetMacSize()]; + this._hmac.BlockUpdate(value, 0, value.Length); + this._hmac.DoFinal(resBuf, 0); + + return resBuf; + } + } +} diff --git a/src/AdsPush.Vapid/Util/JwsSigner.cs b/src/AdsPush.Vapid/Util/JwsSigner.cs new file mode 100644 index 0000000..51c4dbc --- /dev/null +++ b/src/AdsPush.Vapid/Util/JwsSigner.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Newtonsoft.Json; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; + +namespace AdsPush.Vapid.Util +{ + internal class JwsSigner + { + private readonly ECPrivateKeyParameters _privateKey; + + public JwsSigner(ECPrivateKeyParameters privateKey) + { + this._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, this._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(JsonConvert.SerializeObject(header))); + var encodePayload = UrlBase64.Encode(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(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/src/AdsPush.Vapid/Util/UrlBase64.cs b/src/AdsPush.Vapid/Util/UrlBase64.cs new file mode 100644 index 0000000..c610ec4 --- /dev/null +++ b/src/AdsPush.Vapid/Util/UrlBase64.cs @@ -0,0 +1,34 @@ +using System; + +namespace AdsPush.Vapid.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/src/AdsPush.Vapid/VapidHelper.cs b/src/AdsPush.Vapid/VapidHelper.cs new file mode 100644 index 0000000..8c03cc7 --- /dev/null +++ b/src/AdsPush.Vapid/VapidHelper.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using AdsPush.Vapid.Util; + +namespace AdsPush.Vapid +{ + public static class VapidHelper + { + /// + /// 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) + { + 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; + } + + private static void ValidateAudience( + string audience) + { + if (string.IsNullOrEmpty(audience)) + { + throw new ArgumentException(@"No audience could be generated for VAPID."); + } + + if (audience.Length == 0) + { + throw new ArgumentException( + @"The audience value must be a string containing the origin of a push service. " + audience); + } + + if (!Uri.IsWellFormedUriString(audience, UriKind.Absolute)) + { + throw new ArgumentException(@"VAPID audience is not a url."); + } + } + + private static void ValidateSubject( + string subject) + { + 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"); + } + } + } + + private static void ValidatePublicKey( + string publicKey) + { + if (string.IsNullOrEmpty(publicKey)) + { + throw new ArgumentException(@"Valid public key not set"); + } + + var decodedPublicKey = UrlBase64.Decode(publicKey); + + if (decodedPublicKey.Length != 65) + { + throw new ArgumentException(@"Vapid public key must be 65 characters long when decoded"); + } + } + + private static void ValidatePrivateKey( + string privateKey) + { + if (string.IsNullOrEmpty(privateKey)) + { + throw new ArgumentException(@"Valid private key not set"); + } + + var decodedPrivateKey = UrlBase64.Decode(privateKey); + + if (decodedPrivateKey.Length != 32) + { + throw new ArgumentException(@"Vapid private key should be 32 bytes long when decoded."); + } + } + + private static void ValidateExpiration( + long expiration) + { + if (expiration <= UnixTimeNow()) + { + throw new ArgumentException(@"Vapid expiration must be a unix timestamp in the future"); + } + } + + private static long UnixTimeNow() + { + var timeSpan = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0); + return (long)timeSpan.TotalSeconds; + } + + 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; + } + } +} diff --git a/src/AdsPush.Vapid/VapidPushNotificationSender.cs b/src/AdsPush.Vapid/VapidPushNotificationSender.cs new file mode 100644 index 0000000..90f37bc --- /dev/null +++ b/src/AdsPush.Vapid/VapidPushNotificationSender.cs @@ -0,0 +1,180 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using AdsPush.Abstraction; +using AdsPush.Abstraction.Settings; +using AdsPush.Abstraction.Vapid; +using AdsPush.Vapid.Extensions; +using AdsPush.Vapid.Util; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; + +namespace AdsPush.Vapid +{ + public class VapidPushNotificationSender : IVapidPushNotificationSender + { + private readonly HttpClient _client; + private readonly AdsPushVapidSettings _adsPushVapidSettings; + + private readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + NullValueHandling = NullValueHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Ignore + }; + + public VapidPushNotificationSender( + AdsPushVapidSettings adsPushVapidSettings, + HttpClient client) + { + this._client = client; + this._adsPushVapidSettings = adsPushVapidSettings; + } + + /// + public Task SendAsync( + VapidSubscription subscription, + string payload, + CancellationToken cancellationToken = default) + { + return this.SendBaseAsync( + subscription, + payload, + cancellationToken); + } + + /// + public Task SendAsync( + VapidSubscription subscription, + VapidRequest payload, + CancellationToken cancellationToken = default) + { + var jsonPayload = JsonConvert.SerializeObject(payload, this._jsonSerializerSettings); + return this.SendBaseAsync( + subscription, + jsonPayload, + cancellationToken); + } + + /// + public async Task SendAsync( + string subscriptionJson, + AdsPushBasicSendPayload payload, + CancellationToken cancellationToken = default) + { + var subscription = VapidSubscription.FromSubscriptionJson(subscriptionJson); + var vapidRequest = payload.CreateRequest(); + var jsonPayload = JsonConvert.SerializeObject(vapidRequest); + var result = await this.SendBaseAsync( + subscription, + jsonPayload, + cancellationToken); + + if (!result.IsSuccess) + { + throw result.Error.CreateException(); + } + } + + private async Task SendHttpRequestAsync( + VapidSubscription subscription, + string payload, + CancellationToken cancellationToken) + { + var request = new HttpRequestMessage(HttpMethod.Post, subscription.Endpoint); + var encryptedPayload = Encryptor.Encrypt( + subscription.P256dh, + subscription.Auth, + 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"); + + var uri = new Uri(subscription.Endpoint); + var audience = uri.Scheme + @"://" + uri.Host; + var vapidHeaders = VapidHelper.GetVapidHeaders(audience, + this._adsPushVapidSettings.Subject, + this._adsPushVapidSettings.PublicKey, + this._adsPushVapidSettings.PrivateKey); + + var cryptoKeyHeader = @"dh=" + encryptedPayload.Base64EncodePublicKey() + @";" + vapidHeaders["Crypto-Key"]; + request.Headers.Add("Crypto-Key", cryptoKeyHeader); + request.Headers.Add("Encryption", "salt=" + encryptedPayload.Base64EncodeSalt()); + request.Headers.Add("Authorization", vapidHeaders["Authorization"]); + request.Headers.Add("TTL", this.GetTtl(payload).ToString()); + + return await this._client.SendAsync(request, cancellationToken); + } + + private async Task SendBaseAsync( + VapidSubscription subscription, + string jsonPayload, + CancellationToken cancellationToken) + { + if (!this.ValidateSubscription(subscription)) + { + return new VapidResponse(false, new VapidError(VapidErrorReasonCode.InvalidToken, null)); + } + + var response = await this.SendHttpRequestAsync( + subscription, + jsonPayload, + cancellationToken); + + if (response.IsSuccessStatusCode) + { + return new VapidResponse(true, null); + } + + var reasonCode = this.GetVapidErrorReasonCode(response); + return new VapidResponse(false, new VapidError(reasonCode, response)); + } + + private long GetTtl( + string jsonPayload) + { + var ttlString = JObject.Parse(jsonPayload)["TTL"]?.ToString(); + if (!string.IsNullOrEmpty(ttlString) && long.TryParse(ttlString, out var ttlLong) && ttlLong > 0) + { + return ttlLong; + } + + return this._adsPushVapidSettings.TTL.GetValueOrDefault(43200); + } + + private bool ValidateSubscription( + VapidSubscription subscription) + { + return Uri.IsWellFormedUriString(subscription.Endpoint, UriKind.Absolute) + && !string.IsNullOrEmpty(subscription.P256dh) + && !string.IsNullOrEmpty(subscription.Auth); + } + + private VapidErrorReasonCode GetVapidErrorReasonCode( + HttpResponseMessage response) + { + switch (response.StatusCode) + { + case HttpStatusCode.BadRequest: + return VapidErrorReasonCode.InvalidArgument; + case HttpStatusCode.Unauthorized: + case HttpStatusCode.Forbidden: + return VapidErrorReasonCode.InvalidAuthConfiguration; + + case HttpStatusCode.NotFound: + return VapidErrorReasonCode.InvalidToken; + + case HttpStatusCode.ServiceUnavailable: + return VapidErrorReasonCode.ServiceUnavailable; + default: + return VapidErrorReasonCode.UnknownError; + } + } + } +} diff --git a/src/AdsPush.Vapid/VapidPushNotificationSenderFactory.cs b/src/AdsPush.Vapid/VapidPushNotificationSenderFactory.cs new file mode 100644 index 0000000..e142e4f --- /dev/null +++ b/src/AdsPush.Vapid/VapidPushNotificationSenderFactory.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Concurrent; +using System.Net.Http; +using AdsPush.Abstraction.Settings; +using AdsPush.Vapid.Settings; + +namespace AdsPush.Vapid +{ + public class VapidPushNotificationSenderFactory: IVapidPushNotificationSenderFactory + { + public VapidPushNotificationSenderFactory( + VapidSettingsSection vapidSettingsSection, + HttpClient httpClient) + { + this._vapidPushNotificationSenders = new ConcurrentDictionary(); + this._settings = vapidSettingsSection ?? new VapidSettingsSection(); + this._httpClient = httpClient; + } + + public VapidPushNotificationSenderFactory() + { + this._vapidPushNotificationSenders = new ConcurrentDictionary(); + } + + private readonly HttpClient _httpClient; + private readonly ConcurrentDictionary _vapidPushNotificationSenders; + private readonly VapidSettingsSection _settings; + + /// + public IVapidPushNotificationSender GetSender( + string appName) + { + return this._vapidPushNotificationSenders.GetOrAdd(appName, this.ValueFactory); + } + + private VapidPushNotificationSender ValueFactory( + string arg) + { + if (!this._settings.ContainsKey(arg)) + { + throw new ArgumentException($"{arg} is not defined in settings!"); + } + + var settings = this._settings[arg]; + return new VapidPushNotificationSender(settings, this._httpClient); + } + + /// + public IVapidPushNotificationSender GetSender( + string appName, + AdsPushVapidSettings vapidSettings) + { + return this._vapidPushNotificationSenders.GetOrAdd(appName, new VapidPushNotificationSender(vapidSettings, this._httpClient)); + } + } +} diff --git a/src/AdsPush.Vapid/VapidSubscription.cs b/src/AdsPush.Vapid/VapidSubscription.cs new file mode 100644 index 0000000..bfe62a4 --- /dev/null +++ b/src/AdsPush.Vapid/VapidSubscription.cs @@ -0,0 +1,53 @@ +using Newtonsoft.Json.Linq; + +namespace AdsPush.Vapid +{ + public class VapidSubscription + { + public static VapidSubscription FromParameters( + string endpoint, + string p256dh, + string auth) + { + return new VapidSubscription( + endpoint, + p256dh, + auth); + } + + public static VapidSubscription FromSubscriptionJson( + string subscriptionJson) + { + var jsonObject = JObject.Parse(subscriptionJson); + var endpoint = jsonObject["endpoint"]?.ToString(); + var p256dh = jsonObject["keys.p256dh"]?.ToString(); + var auth = jsonObject["keys.auth"]?.ToString(); + return new VapidSubscription( + endpoint, + p256dh, + auth); + } + + public static VapidSubscription FromBase64EncodedSubscriptionJson( + string base64EncodedSubscriptionJson) + { + var base64EncodedBytes = System.Convert.FromBase64String(base64EncodedSubscriptionJson); + var json = System.Text.Encoding.UTF8.GetString(base64EncodedBytes); + return FromSubscriptionJson(json); + } + + private VapidSubscription( + string endpoint, + string p256dh, + string auth) + { + this.Endpoint = endpoint; + this.P256dh = p256dh; + this.Auth = auth; + } + + public string Endpoint { get; } + public string P256dh { get; } + public string Auth { get; } + } +} diff --git a/src/AdsPush/AdsPush.csproj b/src/AdsPush/AdsPush.csproj index 88477da..8dfa5ff 100644 --- a/src/AdsPush/AdsPush.csproj +++ b/src/AdsPush/AdsPush.csproj @@ -25,6 +25,7 @@ + diff --git a/src/AdsPush/Extensions/BuilderExtension.cs b/src/AdsPush/Extensions/BuilderExtension.cs index 3b15c30..aa86bf7 100644 --- a/src/AdsPush/Extensions/BuilderExtension.cs +++ b/src/AdsPush/Extensions/BuilderExtension.cs @@ -4,12 +4,13 @@ using AdsPush.Abstraction.Settings; using AdsPush.APNS.Extensions; using AdsPush.Firebase.Extensions; +using AdsPush.Vapid.Extensions; using Microsoft.Extensions.Configuration; namespace AdsPush.Extensions { /// - /// + /// /// public static class BuilderExtension { @@ -26,6 +27,7 @@ public static class BuilderExtension { services.AddFirebaseCloudMessagingServiceFactory(); services.AddAppleNotificationServiceFactory(); + services.AddVapidNotificationServiceFactory(); services.AddSingleton(); services.AddSingleton(); services.Configure(settings); @@ -45,6 +47,7 @@ public static class BuilderExtension { services.AddFirebaseCloudMessagingServiceFactory(); services.AddAppleNotificationServiceFactory(); + services.AddVapidNotificationServiceFactory(); services.AddSingleton(); services.AddSingleton(); services.Configure(configuration.GetSection("AdsPush")); @@ -63,6 +66,7 @@ public static class BuilderExtension { services.AddFirebaseCloudMessagingServiceFactory(); services.AddAppleNotificationServiceFactory(); + services.AddVapidNotificationServiceFactory(); services.AddSingleton(); services.AddSingleton(); From 1cd1aadf947e34d99a7a8c68e5ad9af037660d17 Mon Sep 17 00:00:00 2001 From: "Anil.Senel" Date: Tue, 15 Aug 2023 15:48:33 +0300 Subject: [PATCH 2/6] configuration updated --- .../IVapidPushNotificationSender.cs | 34 +++++---- .../IVapidPushNotificationSenderFactory.cs | 2 +- src/AdsPush.Vapid/Util/EncryptionResult.cs | 2 +- src/AdsPush.Vapid/VapidSubscription.cs | 37 +++++++++- src/AdsPush/AdsPushSender.cs | 10 ++- src/AdsPush/AdsPushSenderBuilder.cs | 72 ++++++++++++------- src/AdsPush/AdsPushSenderFactory.cs | 17 +++-- 7 files changed, 118 insertions(+), 56 deletions(-) diff --git a/src/AdsPush.Vapid/IVapidPushNotificationSender.cs b/src/AdsPush.Vapid/IVapidPushNotificationSender.cs index b959e02..0fdfa8a 100644 --- a/src/AdsPush.Vapid/IVapidPushNotificationSender.cs +++ b/src/AdsPush.Vapid/IVapidPushNotificationSender.cs @@ -6,43 +6,41 @@ namespace AdsPush.Vapid { /// - /// Use to commute VAPID Notification supported services. + /// Defines operations for sending VAPID notifications. /// public interface IVapidPushNotificationSender { /// - /// and - /// + /// Sends a VAPID push notification with the provided subscription and payload. /// - /// Use to pass subscription info - /// - /// - /// + /// The subscription information. + /// The payload as a string. + /// The cancellation token. + /// A task representing the asynchronous operation. Task SendAsync( VapidSubscription subscription, string payload, CancellationToken cancellationToken = default); /// - /// and - /// + /// Sends a VAPID push notification with the provided subscription and payload. /// - /// Use to pass subscription info - /// The payload model. - /// - /// + /// The subscription information. + /// The payload as a model. + /// The cancellation token. + /// A task representing the asynchronous operation. Task SendAsync( VapidSubscription subscription, VapidRequest payload, CancellationToken cancellationToken = default); /// - /// + /// Sends a VAPID push notification with the provided subscription JSON and payload. /// - /// - /// - /// - /// + /// The subscription information as JSON. + /// The payload as an model. + /// The cancellation token. + /// A task representing the asynchronous operation. Task SendAsync( string subscriptionJson, AdsPushBasicSendPayload payload, diff --git a/src/AdsPush.Vapid/IVapidPushNotificationSenderFactory.cs b/src/AdsPush.Vapid/IVapidPushNotificationSenderFactory.cs index 4229e00..a6b8eb9 100644 --- a/src/AdsPush.Vapid/IVapidPushNotificationSenderFactory.cs +++ b/src/AdsPush.Vapid/IVapidPushNotificationSenderFactory.cs @@ -1,8 +1,8 @@ using AdsPush.Abstraction.Settings; -using AdsPush.Vapid.Settings; namespace AdsPush.Vapid { + public interface IVapidPushNotificationSenderFactory { IVapidPushNotificationSender GetSender( diff --git a/src/AdsPush.Vapid/Util/EncryptionResult.cs b/src/AdsPush.Vapid/Util/EncryptionResult.cs index 714af3e..4cecc01 100644 --- a/src/AdsPush.Vapid/Util/EncryptionResult.cs +++ b/src/AdsPush.Vapid/Util/EncryptionResult.cs @@ -2,7 +2,7 @@ { // @LogicSoftware // Originally From: https://github.com/LogicSoftware/WebPushEncryption/blob/master/src/EncryptionResult.cs - public class EncryptionResult + internal class EncryptionResult { public byte[] PublicKey { get; set; } public byte[] Payload { get; set; } diff --git a/src/AdsPush.Vapid/VapidSubscription.cs b/src/AdsPush.Vapid/VapidSubscription.cs index bfe62a4..017bf19 100644 --- a/src/AdsPush.Vapid/VapidSubscription.cs +++ b/src/AdsPush.Vapid/VapidSubscription.cs @@ -2,8 +2,18 @@ namespace AdsPush.Vapid { + /// + /// Represents a VAPID subscription used for sending push notifications. + /// public class VapidSubscription { + /// + /// Creates a new instance of using the provided parameters. + /// + /// The URL endpoint of the subscription. + /// The p256dh value of the subscription. + /// The auth value of the subscription. + /// A new instance. public static VapidSubscription FromParameters( string endpoint, string p256dh, @@ -15,6 +25,11 @@ public class VapidSubscription auth); } + /// + /// Creates a new instance of by parsing the subscription JSON. + /// + /// The JSON representation of the subscription. + /// A new instance. public static VapidSubscription FromSubscriptionJson( string subscriptionJson) { @@ -28,6 +43,11 @@ public class VapidSubscription auth); } + /// + /// Creates a new instance of by parsing the base64-encoded subscription JSON. + /// + /// The base64-encoded JSON representation of the subscription. + /// A new instance. public static VapidSubscription FromBase64EncodedSubscriptionJson( string base64EncodedSubscriptionJson) { @@ -41,13 +61,24 @@ public class VapidSubscription string p256dh, string auth) { - this.Endpoint = endpoint; - this.P256dh = p256dh; - this.Auth = auth; + Endpoint = endpoint; + P256dh = p256dh; + Auth = auth; } + /// + /// Gets the URL endpoint of the subscription. + /// public string Endpoint { get; } + + /// + /// Gets the p256dh value of the subscription. + /// public string P256dh { get; } + + /// + /// Gets the auth value of the subscription. + /// public string Auth { get; } } } diff --git a/src/AdsPush/AdsPushSender.cs b/src/AdsPush/AdsPushSender.cs index 5b8b053..6bf8a12 100644 --- a/src/AdsPush/AdsPushSender.cs +++ b/src/AdsPush/AdsPushSender.cs @@ -4,11 +4,12 @@ using AdsPush.Abstraction; using AdsPush.APNS; using AdsPush.Firebase; +using AdsPush.Vapid; namespace AdsPush { /// - /// + /// /// public class AdsPushSender : IAdsPushSender { @@ -16,9 +17,10 @@ public class AdsPushSender : IAdsPushSender private readonly IAdsPushConfigurationProvider _adsPushConfigurationProvider; private readonly IFirebasePushNotificationSenderFactory _firebasePushNotificationSenderFactory; private readonly IApplePushNotificationSenderFactory _applePushNotificationSenderFactory; + private readonly IVapidPushNotificationSenderFactory _vapidPushNotificationSenderFactory; /// - /// + /// /// /// /// @@ -28,12 +30,14 @@ public class AdsPushSender : IAdsPushSender string appName, IAdsPushConfigurationProvider adsPushConfigurationProvider, IFirebasePushNotificationSenderFactory firebasePushNotificationSenderFactory, - IApplePushNotificationSenderFactory applePushNotificationSenderFactory) + IApplePushNotificationSenderFactory applePushNotificationSenderFactory, + IVapidPushNotificationSenderFactory vapidPushNotificationSenderFactory) { this._appName = appName; this._adsPushConfigurationProvider = adsPushConfigurationProvider; this._firebasePushNotificationSenderFactory = firebasePushNotificationSenderFactory; this._applePushNotificationSenderFactory = applePushNotificationSenderFactory; + this._vapidPushNotificationSenderFactory = vapidPushNotificationSenderFactory; } diff --git a/src/AdsPush/AdsPushSenderBuilder.cs b/src/AdsPush/AdsPushSenderBuilder.cs index f350efb..99f2b1f 100644 --- a/src/AdsPush/AdsPushSenderBuilder.cs +++ b/src/AdsPush/AdsPushSenderBuilder.cs @@ -6,6 +6,8 @@ using AdsPush.APNS.Settings; using AdsPush.Firebase; using AdsPush.Firebase.Settings; +using AdsPush.Vapid; +using AdsPush.Vapid.Settings; namespace AdsPush { @@ -16,15 +18,16 @@ public class AdsPushSenderBuilder { private readonly AdsPushAppSettings _adsPushAppSettings; private HttpClient _apnsHttpClient; - + private HttpClient _vapidHttpClient; + /// - /// + /// /// public AdsPushSenderBuilder() { - _adsPushAppSettings = new AdsPushAppSettings(); + this._adsPushAppSettings = new AdsPushAppSettings(); } - + /// /// Use to configure APNS for sender. /// @@ -35,13 +38,24 @@ public AdsPushSenderBuilder() AdsPushAPNSSettings settings, HttpClient httpClient = null) { - _adsPushAppSettings.Apns = settings; - _adsPushAppSettings.TargetMappings.Add(AdsPushTarget.Ios, AdsPushProvider.Apns); - _apnsHttpClient = httpClient ?? new HttpClient(); - + this._adsPushAppSettings.Apns = settings; + this._adsPushAppSettings.TargetMappings.Add(AdsPushTarget.Ios, AdsPushProvider.Apns); + this._apnsHttpClient = httpClient ?? new HttpClient(); + + return this; + } + + public AdsPushSenderBuilder ConfigureVapid( + AdsPushVapidSettings settings, + HttpClient httpClient = null) + { + this._adsPushAppSettings.Vapid = settings; + this._adsPushAppSettings.TargetMappings.Add(AdsPushTarget.BrowserAndPwa, AdsPushProvider.VapidWebPush); + this._vapidHttpClient = httpClient ?? new HttpClient(); + return this; } - + /// /// Use to configure Firebase Cloud Messaging for sender. /// @@ -52,15 +66,15 @@ public AdsPushSenderBuilder() AdsPushFirebaseSettings settings, params AdsPushTarget[] targets) { - _adsPushAppSettings.Firebase = settings; + this._adsPushAppSettings.Firebase = settings; foreach (var target in targets) { - _adsPushAppSettings.TargetMappings[target] = AdsPushProvider.Firebase; + this._adsPushAppSettings.TargetMappings[target] = AdsPushProvider.Firebase; } - + return this; } - + /// /// Build the configured sender. /// @@ -68,31 +82,41 @@ public AdsPushSenderBuilder() public IAdsPushSender BuildSender() { var appName = Guid.NewGuid().ToString(); - var provider = new BasicAdsPushConfigurationProvider(_adsPushAppSettings); - - var apnsFactory = _adsPushAppSettings.Apns != null + var provider = new BasicAdsPushConfigurationProvider(this._adsPushAppSettings); + + var apnsFactory = this._adsPushAppSettings.Apns != null ? new ApplePushNotificationSenderFactory(new APNSSettingsSection { { - appName, _adsPushAppSettings.Apns + appName, this._adsPushAppSettings.Apns } - }, _apnsHttpClient) + }, this._apnsHttpClient) : null; - - var firebaseFactory = _adsPushAppSettings.Firebase != null + + var firebaseFactory = this._adsPushAppSettings.Firebase != null ? new FirebasePushNotificationSenderFactory(new FirebaseAppSettingsSection { { - appName, _adsPushAppSettings.Firebase + appName, this._adsPushAppSettings.Firebase } }) : null; - + + var vapidFactory = this._adsPushAppSettings.Vapid != null + ? new VapidPushNotificationSenderFactory(new VapidSettingsSection() + { + { + appName, this._adsPushAppSettings.Vapid + } + }, this._vapidHttpClient) + : null; + return new AdsPushSender( appName, provider, firebaseFactory, - apnsFactory); + apnsFactory, + vapidFactory); } } -} \ No newline at end of file +} diff --git a/src/AdsPush/AdsPushSenderFactory.cs b/src/AdsPush/AdsPushSenderFactory.cs index 70a1cc8..5363fda 100644 --- a/src/AdsPush/AdsPushSenderFactory.cs +++ b/src/AdsPush/AdsPushSenderFactory.cs @@ -1,8 +1,8 @@ using System.Collections.Concurrent; using AdsPush.Abstraction; -using AdsPush.Abstraction.Settings; using AdsPush.APNS; using AdsPush.Firebase; +using AdsPush.Vapid; namespace AdsPush { @@ -15,25 +15,29 @@ public class AdsPushSenderFactory : IAdsPushSenderFactory private readonly IAdsPushConfigurationProvider _adsPushConfigurationProvider; private readonly IApplePushNotificationSenderFactory _applePushNotificationSenderFactory; private readonly IFirebasePushNotificationSenderFactory _firebasePushNotificationSenderFactory; - + private readonly IVapidPushNotificationSenderFactory _vapidPushNotificationSenderFactory; + /// - /// + /// /// /// /// /// + /// public AdsPushSenderFactory( IAdsPushConfigurationProvider adsPushConfigurationProvider, IApplePushNotificationSenderFactory applePushNotificationSenderFactory, - IFirebasePushNotificationSenderFactory firebasePushNotificationSenderFactory) + IFirebasePushNotificationSenderFactory firebasePushNotificationSenderFactory, + IVapidPushNotificationSenderFactory vapidPushNotificationSenderFactory) { this._senders = new ConcurrentDictionary(); this._adsPushConfigurationProvider = adsPushConfigurationProvider; this._applePushNotificationSenderFactory = applePushNotificationSenderFactory; this._firebasePushNotificationSenderFactory = firebasePushNotificationSenderFactory; + this._vapidPushNotificationSenderFactory = vapidPushNotificationSenderFactory; } - + /// public IAdsPushSender GetSender( string appName) @@ -43,7 +47,8 @@ public class AdsPushSenderFactory : IAdsPushSenderFactory new AdsPushSender( appName, this._adsPushConfigurationProvider, this._firebasePushNotificationSenderFactory, - this._applePushNotificationSenderFactory)); + this._applePushNotificationSenderFactory, + this._vapidPushNotificationSenderFactory)); } } } From 431d7bc0e9222124d59a0b56967e5d38318ef4f4 Mon Sep 17 00:00:00 2001 From: "Anil.Senel" Date: Tue, 15 Aug 2023 16:47:20 +0300 Subject: [PATCH 3/6] fix --- AdsPush.sln | 7 ++++++ .../VapidPushNotificationSender.cs | 2 +- src/AdsPush.Vapid/VapidSubscription.cs | 25 ++++++++++++++----- src/AdsPush/AdsPushSender.cs | 19 +++++++++++++- 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/AdsPush.sln b/AdsPush.sln index 490cd7f..a4e34d5 100644 --- a/AdsPush.sln +++ b/AdsPush.sln @@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdsPushSample.Api", "sample EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdsPushSample.ConsoleApp", "samples\AdsPushSample.ConsoleApp\AdsPushSample.ConsoleApp.csproj", "{CE11B712-AD05-42CD-83C4-1183CCABB081}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdsPush.Vapid", "src\AdsPush.Vapid\AdsPush.Vapid.csproj", "{5EC17E88-BF46-4822-89AE-CC4B401128D0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -54,6 +56,10 @@ Global {CE11B712-AD05-42CD-83C4-1183CCABB081}.Debug|Any CPU.Build.0 = Debug|Any CPU {CE11B712-AD05-42CD-83C4-1183CCABB081}.Release|Any CPU.ActiveCfg = Release|Any CPU {CE11B712-AD05-42CD-83C4-1183CCABB081}.Release|Any CPU.Build.0 = Release|Any CPU + {5EC17E88-BF46-4822-89AE-CC4B401128D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5EC17E88-BF46-4822-89AE-CC4B401128D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5EC17E88-BF46-4822-89AE-CC4B401128D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5EC17E88-BF46-4822-89AE-CC4B401128D0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {33EC0E05-DEC5-499C-9B58-3648A09C03A7} = {6DCD587C-FB77-4254-B98C-8E3CBC508EF1} @@ -62,5 +68,6 @@ Global {95D40230-2B98-474B-8525-1C4B7BBB7337} = {6DCD587C-FB77-4254-B98C-8E3CBC508EF1} {0C9EC007-C443-415C-B7DA-D0959E4B3292} = {27B12BE4-A809-42C0-A6DC-8306BB2206BF} {CE11B712-AD05-42CD-83C4-1183CCABB081} = {27B12BE4-A809-42C0-A6DC-8306BB2206BF} + {5EC17E88-BF46-4822-89AE-CC4B401128D0} = {6DCD587C-FB77-4254-B98C-8E3CBC508EF1} EndGlobalSection EndGlobal diff --git a/src/AdsPush.Vapid/VapidPushNotificationSender.cs b/src/AdsPush.Vapid/VapidPushNotificationSender.cs index 90f37bc..01a5537 100644 --- a/src/AdsPush.Vapid/VapidPushNotificationSender.cs +++ b/src/AdsPush.Vapid/VapidPushNotificationSender.cs @@ -68,7 +68,7 @@ public class VapidPushNotificationSender : IVapidPushNotificationSender { var subscription = VapidSubscription.FromSubscriptionJson(subscriptionJson); var vapidRequest = payload.CreateRequest(); - var jsonPayload = JsonConvert.SerializeObject(vapidRequest); + var jsonPayload = JsonConvert.SerializeObject(vapidRequest, this._jsonSerializerSettings); var result = await this.SendBaseAsync( subscription, jsonPayload, diff --git a/src/AdsPush.Vapid/VapidSubscription.cs b/src/AdsPush.Vapid/VapidSubscription.cs index 017bf19..77583fb 100644 --- a/src/AdsPush.Vapid/VapidSubscription.cs +++ b/src/AdsPush.Vapid/VapidSubscription.cs @@ -34,9 +34,9 @@ public class VapidSubscription string subscriptionJson) { var jsonObject = JObject.Parse(subscriptionJson); - var endpoint = jsonObject["endpoint"]?.ToString(); - var p256dh = jsonObject["keys.p256dh"]?.ToString(); - var auth = jsonObject["keys.auth"]?.ToString(); + var endpoint = jsonObject.SelectToken("endpoint")?.ToString(); + var p256dh = jsonObject.SelectToken("keys.p256dh")?.ToString(); + var auth = jsonObject.SelectToken("keys.auth")?.ToString(); return new VapidSubscription( endpoint, p256dh, @@ -61,9 +61,22 @@ public class VapidSubscription string p256dh, string auth) { - Endpoint = endpoint; - P256dh = p256dh; - Auth = auth; + this.Endpoint = endpoint; + this.P256dh = p256dh; + this.Auth = auth; + } + + public string ToAdsPushToken() + { + return new JObject() + { + ["endpoint"] = this.Endpoint, + ["keys"] = new JObject() + { + ["auth"] = this.Auth, + ["p256dh"] = this.P256dh + } + }.ToString(); } /// diff --git a/src/AdsPush/AdsPushSender.cs b/src/AdsPush/AdsPushSender.cs index 6bf8a12..9ae5540 100644 --- a/src/AdsPush/AdsPushSender.cs +++ b/src/AdsPush/AdsPushSender.cs @@ -100,6 +100,23 @@ await this._firebasePushNotificationSenderFactory payload, cancellationToken); + break; + case AdsPushProvider.VapidWebPush: + if (settings.Vapid is null) + { + throw new AdsPushException( + $"Settings are not configured for target platform {target}. Configure VAPID to be able to proceed.", + AdsPushErrorType.InvalidAuthConfiguration, + null); + } + + await this._vapidPushNotificationSenderFactory + .GetSender(this._appName, settings.Vapid) + .SendAsync( + pushToken, + payload, + cancellationToken); + break; default: throw new NotSupportedException($"Target {target} is not supported by Framework"); @@ -109,7 +126,7 @@ await this._firebasePushNotificationSenderFactory /// public IApplePushNotificationSender GetApnsSender() { - return _applePushNotificationSenderFactory.GetSender(this._appName); + return this._applePushNotificationSenderFactory.GetSender(this._appName); } /// From 6e9f056e3813649504164c01ff018a1ebece904e Mon Sep 17 00:00:00 2001 From: "Anil.Senel" Date: Sun, 20 Aug 2023 17:13:27 +0300 Subject: [PATCH 4/6] . --- AdsPush.sln | 14 +++ samples/AdsPushSample.ConsoleApp/Program.cs | 42 +++++++- .../AdsPushSample.VapidClient/icon-180.png | Bin 0 -> 7116 bytes samples/AdsPushSample.VapidClient/icon.png | Bin 0 -> 7302 bytes samples/AdsPushSample.VapidClient/index.html | 96 ++++++++++++++++++ .../AdsPushSample.VapidClient/manifest.json | 18 ++++ .../sample-subscription.json | 7 ++ .../service-worker.js | 39 +++++++ .../splash-image.jpg | Bin 0 -> 61251 bytes samples/AdsPushSample.VapidClient/splash.css | 19 ++++ samples/AdsPushSample.VapidClient/splash.html | 12 +++ .../AdsPushBasicSendPayload.cs | 15 ++- src/AdsPush.Abstraction/Vapid/VapidRequest.cs | 7 +- .../Extensions/MappingExtension.cs | 22 ++-- .../Extensions/MappingExtensions.cs | 2 + src/AdsPush.Vapid/VapidHelper.cs | 15 +++ src/AdsPush.Vapid/VapidKeyGenerationResult.cs | 27 +++++ src/AdsPush/AdsPushSender.cs | 6 ++ src/AdsPush/IAdsPushSender.cs | 14 ++- 19 files changed, 338 insertions(+), 17 deletions(-) create mode 100644 samples/AdsPushSample.VapidClient/icon-180.png create mode 100644 samples/AdsPushSample.VapidClient/icon.png create mode 100644 samples/AdsPushSample.VapidClient/index.html create mode 100644 samples/AdsPushSample.VapidClient/manifest.json create mode 100644 samples/AdsPushSample.VapidClient/sample-subscription.json create mode 100644 samples/AdsPushSample.VapidClient/service-worker.js create mode 100644 samples/AdsPushSample.VapidClient/splash-image.jpg create mode 100644 samples/AdsPushSample.VapidClient/splash.css create mode 100644 samples/AdsPushSample.VapidClient/splash.html create mode 100644 src/AdsPush.Vapid/VapidKeyGenerationResult.cs diff --git a/AdsPush.sln b/AdsPush.sln index a4e34d5..966266a 100644 --- a/AdsPush.sln +++ b/AdsPush.sln @@ -23,6 +23,19 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdsPushSample.ConsoleApp", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdsPush.Vapid", "src\AdsPush.Vapid\AdsPush.Vapid.csproj", "{5EC17E88-BF46-4822-89AE-CC4B401128D0}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AdsPushSample.VapidClient", "AdsPushSample.VapidClient", "{8D84A155-DF61-4E97-A847-9CD9049E55AE}" + ProjectSection(SolutionItems) = preProject + samples\AdsPushSample.VapidClient\icon-180.png = samples\AdsPushSample.VapidClient\icon-180.png + samples\AdsPushSample.VapidClient\icon.png = samples\AdsPushSample.VapidClient\icon.png + samples\AdsPushSample.VapidClient\index.html = samples\AdsPushSample.VapidClient\index.html + samples\AdsPushSample.VapidClient\manifest.json = samples\AdsPushSample.VapidClient\manifest.json + samples\AdsPushSample.VapidClient\sample-subscription.json = samples\AdsPushSample.VapidClient\sample-subscription.json + samples\AdsPushSample.VapidClient\service-worker.js = samples\AdsPushSample.VapidClient\service-worker.js + samples\AdsPushSample.VapidClient\splash-image.jpg = samples\AdsPushSample.VapidClient\splash-image.jpg + samples\AdsPushSample.VapidClient\splash.html = samples\AdsPushSample.VapidClient\splash.html + samples\AdsPushSample.VapidClient\splash.css = samples\AdsPushSample.VapidClient\splash.css + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,5 +82,6 @@ Global {0C9EC007-C443-415C-B7DA-D0959E4B3292} = {27B12BE4-A809-42C0-A6DC-8306BB2206BF} {CE11B712-AD05-42CD-83C4-1183CCABB081} = {27B12BE4-A809-42C0-A6DC-8306BB2206BF} {5EC17E88-BF46-4822-89AE-CC4B401128D0} = {6DCD587C-FB77-4254-B98C-8E3CBC508EF1} + {8D84A155-DF61-4E97-A847-9CD9049E55AE} = {27B12BE4-A809-42C0-A6DC-8306BB2206BF} EndGlobalSection EndGlobal diff --git a/samples/AdsPushSample.ConsoleApp/Program.cs b/samples/AdsPushSample.ConsoleApp/Program.cs index 0802730..023bd3a 100644 --- a/samples/AdsPushSample.ConsoleApp/Program.cs +++ b/samples/AdsPushSample.ConsoleApp/Program.cs @@ -6,6 +6,7 @@ using AdsPush.Abstraction; using AdsPush.Abstraction.APNS; using AdsPush.Abstraction.Settings; +using AdsPush.Vapid; using FirebaseAdmin.Messaging; var builder = new AdsPushSenderBuilder(); @@ -19,11 +20,50 @@ //put your configurations hare. }; + +var publicKey = "BF59A9jkMtVqs0Gzef1o6xhcB8SBHjhufCLikJhtNY9YGl_Zm2PwLMYbQs_RvD3T0yUFUlcFBt6nqSVOdoU05IM"; +var privateKey = "jYJABdhwbgAOiQkz97LK39FjA5YF4WXPxcgDX7bdRcQ"; +var subject = @"mailto:example@example.com"; +var vapidSettings = new AdsPushVapidSettings() +{ + //put your configurations hare. + PublicKey = publicKey, + PrivateKey = privateKey, + Subject = subject +}; + var sender = builder + .ConfigureVapid(vapidSettings, null) .ConfigureApns(apnsSettings, null) .ConfigureFirebase(firebaseSettings, AdsPushTarget.Android) .BuildSender(); + +// string +// endpoint = "https://fcm.googleapis.com/fcm/send/cIo6QJ4MMtQ:APA91bEGHCpZdHaUS7otb5_xU1zNWe6TAqria9phFm7M_9ZIiEyr0vXj3gRHbeIJMYvp2-SAVbgNrVvl7uBvU_VTLpIA0CLBcmqXuuEktGr0U4LVLvwWBibO68spJk7D-lr8R9zPyAXE", +// p256dh = "BIjydse4Rij892SJN10xx1qbxDM6GrYXSfg7TGu90CVM1WmlTYzn_79psRqseyWdER969LGLjZmnXIhHPaKTyGE", +// auth = "TkLGLzFeUU3C9SJJN6dLAA"; + + + +//Safari mobile +string + endpoint = "", + p256dh = "", + auth = ""; + + +var subs = VapidSubscription.FromParameters(endpoint, p256dh, auth); +await sender.BasicSendAsync( + AdsPushTarget.BrowserAndPwa, + subs.ToAdsPushToken(), + new AdsPushBasicSendPayload() + { + Title = AdsPushText.CreateUsingString("Title"), + Detail = AdsPushText.CreateUsingString("Detail"), + + }); + var apnDeviceToken = "15f6fdd0f34a7e0f46301a817536f0fb1b2ab05b09b3fae02beba2854a1a2a16"; await sender.BasicSendAsync( @@ -101,4 +141,4 @@ Body = "", ImageUrl = "" } - }); \ No newline at end of file + }); diff --git a/samples/AdsPushSample.VapidClient/icon-180.png b/samples/AdsPushSample.VapidClient/icon-180.png new file mode 100644 index 0000000000000000000000000000000000000000..caf3cfa12e2b0f3fed89f10e9892279436ae783c GIT binary patch literal 7116 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 literal 0 HcmV?d00001 diff --git a/samples/AdsPushSample.VapidClient/index.html b/samples/AdsPushSample.VapidClient/index.html new file mode 100644 index 0000000..b8a1a93 --- /dev/null +++ b/samples/AdsPushSample.VapidClient/index.html @@ -0,0 +1,96 @@ + + + + + + + + + + + AdsPush Vapid Client Test + + + +
+

AdsPush Sample App

+ +
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + + diff --git a/samples/AdsPushSample.VapidClient/manifest.json b/samples/AdsPushSample.VapidClient/manifest.json new file mode 100644 index 0000000..637e9fe --- /dev/null +++ b/samples/AdsPushSample.VapidClient/manifest.json @@ -0,0 +1,18 @@ +{ + "name": "AdsPushVapidClient", + "short_name": "AdsPushVapidClient", + "start_url": ".", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#000000", + "icons": [ + { + "src": "icon.png", + "sizes": "192x192", + "type": "image/png" + } + ], + "splash_pages": { + "normal": "splash.html" + } +} diff --git a/samples/AdsPushSample.VapidClient/sample-subscription.json b/samples/AdsPushSample.VapidClient/sample-subscription.json new file mode 100644 index 0000000..c345413 --- /dev/null +++ b/samples/AdsPushSample.VapidClient/sample-subscription.json @@ -0,0 +1,7 @@ +{ + "endpoint": "<...>", + "keys": { + "auth": "<...>", + "p256dh": "<...>" + } +} diff --git a/samples/AdsPushSample.VapidClient/service-worker.js b/samples/AdsPushSample.VapidClient/service-worker.js new file mode 100644 index 0000000..3c3dd50 --- /dev/null +++ b/samples/AdsPushSample.VapidClient/service-worker.js @@ -0,0 +1,39 @@ +self.addEventListener("push", (event) => { + if (!(self.Notification && self.Notification.permission === "granted")) { + return; + } + + const data = event.data?.json() ?? {}; + const icon = "icon.png"; + + const options = { + lang: data.lang || "en-US", + title: data.title, + body: data.message, + tag: data.tag, + image: data.image, + vibrate: [200, 100, 200], + actions: data.actions || [], + icon, + data: { + url: data.click_action // Bu veriyi tıklandığında kullanıyoruz + } + }; + + event.waitUntil(self.registration.showNotification(data.title, options)); +}); + +self.addEventListener('notificationclick', function (event) { + event.notification.close(); // Bildirimi kapat + if (clients.openWindow && event.notification.data.url) { + event.waitUntil(clients.openWindow(event.notification.data.url)); + } +}); + +self.addEventListener('install', function (event) { + self.skipWaiting(); // Hemen yüklenmesini sağla +}); + +self.addEventListener('activate', function (event) { + event.waitUntil(clients.claim()); // Hemen etkinleşmesini sağla +}); diff --git a/samples/AdsPushSample.VapidClient/splash-image.jpg b/samples/AdsPushSample.VapidClient/splash-image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ca7859f95b117e1b4cf57c41cacb994fe40e6e30 GIT binary patch literal 61251 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 + + + Splash Screen + + + +

+ Splash Screen +
+ + diff --git a/src/AdsPush.Abstraction/AdsPushBasicSendPayload.cs b/src/AdsPush.Abstraction/AdsPushBasicSendPayload.cs index 4cc1017..2441c35 100644 --- a/src/AdsPush.Abstraction/AdsPushBasicSendPayload.cs +++ b/src/AdsPush.Abstraction/AdsPushBasicSendPayload.cs @@ -9,7 +9,7 @@ namespace AdsPush.Abstraction public class AdsPushBasicSendPayload { /// - /// + /// /// public AdsPushBasicSendPayload() { @@ -20,7 +20,7 @@ public AdsPushBasicSendPayload() /// Unique notification id. ///
public string Id { get; set; } = Guid.NewGuid().ToString(); - + /// /// Basic notification tyoe. /// @@ -43,10 +43,10 @@ public AdsPushBasicSendPayload() /// Sound file name for notification. /// public string Sound { get; set; } - + /// /// Related group id to be able to group notification. - /// Ios thread id + /// Ios thread id /// public string GroupId { get; set; } @@ -60,6 +60,11 @@ public AdsPushBasicSendPayload() /// /// public Dictionary Parameters { get; set; } - + + /// + /// Time to leve for notification. + /// Pass null for platform default. + /// + public TimeSpan? Ttl { get; set; } } } diff --git a/src/AdsPush.Abstraction/Vapid/VapidRequest.cs b/src/AdsPush.Abstraction/Vapid/VapidRequest.cs index 6dd1b4c..92682b3 100644 --- a/src/AdsPush.Abstraction/Vapid/VapidRequest.cs +++ b/src/AdsPush.Abstraction/Vapid/VapidRequest.cs @@ -47,13 +47,18 @@ public class VapidRequest /// /// Time to live for the notification, in seconds. Defines how long a push message is retained if the user's device is offline. If not delivered in this time, the message will be dropped. /// - public int? TTL { get; set; } + public long? TTL { get; set; } /// /// Indicates whether the notification requires user interaction. /// public bool RequireInteraction { get; set; } + /// + /// Set the notification background or not. + /// + public bool Silent { get; set; } + /// /// Pattern for the vibration (if supported). /// diff --git a/src/AdsPush.Firebase/Extensions/MappingExtension.cs b/src/AdsPush.Firebase/Extensions/MappingExtension.cs index aad4274..ece4cf3 100644 --- a/src/AdsPush.Firebase/Extensions/MappingExtension.cs +++ b/src/AdsPush.Firebase/Extensions/MappingExtension.cs @@ -5,6 +5,7 @@ using FirebaseAdmin.Auth; using FirebaseAdmin.Messaging; using AdsPush.Abstraction; +using AdsPush.Abstraction.APNS; namespace AdsPush.Firebase.Extensions { @@ -48,15 +49,21 @@ public static class MappingExtension LocKey = payload.Detail.LocalizationKey, LocArgs = payload.Detail.LocalizationArgs, } - }, - Headers = new Dictionary() + } + }; + var headers = new Dictionary() + { { - { - "apns-push-type", payload.PushType.ToString() - } - }, + "apns-push-type", payload.PushType.ToString() + } }; + if (payload.Ttl.HasValue) + { + headers.Add("apns-expiration", APNSExpiration.FromTimeSpan(payload.Ttl.Value).ApnsExpirationValue.ToString()); + } + + message.Apns.Headers = headers; break; case AdsPushTarget.Android: message.Android = new AndroidConfig() @@ -75,6 +82,7 @@ public static class MappingExtension Sound = payload.Sound, }, Data = payload.Parameters.ToDictionary(x => x.Key, x => x.Value.ToString()), + TimeToLive = payload.Ttl }; break; @@ -140,4 +148,4 @@ public static class MappingExtension } } } -} \ No newline at end of file +} diff --git a/src/AdsPush.Vapid/Extensions/MappingExtensions.cs b/src/AdsPush.Vapid/Extensions/MappingExtensions.cs index bf3848f..3def5f7 100644 --- a/src/AdsPush.Vapid/Extensions/MappingExtensions.cs +++ b/src/AdsPush.Vapid/Extensions/MappingExtensions.cs @@ -54,6 +54,8 @@ public static class MappingExtensions Tag = payload.GroupId, Sound = payload.Sound, Data = payload.Parameters.ToDictionary(x=>x.Key, x=>x.Value.ToString()), + Silent = payload.PushType is AdsPushType.Background, + TTL = (long?)payload.Ttl?.TotalSeconds }; } } diff --git a/src/AdsPush.Vapid/VapidHelper.cs b/src/AdsPush.Vapid/VapidHelper.cs index 8c03cc7..867755b 100644 --- a/src/AdsPush.Vapid/VapidHelper.cs +++ b/src/AdsPush.Vapid/VapidHelper.cs @@ -1,11 +1,26 @@ using System; using System.Collections.Generic; using AdsPush.Vapid.Util; +using Org.BouncyCastle.Crypto.Parameters; namespace AdsPush.Vapid { public static class VapidHelper { + /// + /// Generate vapid keys + /// + public static VapidKeyGenerationResult GenerateVapidKeys() + { + var keys = ECKeyHelper.GenerateKeys(); + var publicKey = ((ECPublicKeyParameters) keys.Public).Q.GetEncoded(false); + var privateKey = ((ECPrivateKeyParameters) keys.Private).D.ToByteArrayUnsigned(); + + return new VapidKeyGenerationResult( + UrlBase64.Encode(publicKey), + UrlBase64.Encode(ByteArrayPadLeft(privateKey, 32))); + } + /// /// This method takes the required VAPID parameters and returns the required /// header to be added to a Web Push Protocol Request. diff --git a/src/AdsPush.Vapid/VapidKeyGenerationResult.cs b/src/AdsPush.Vapid/VapidKeyGenerationResult.cs new file mode 100644 index 0000000..68c2462 --- /dev/null +++ b/src/AdsPush.Vapid/VapidKeyGenerationResult.cs @@ -0,0 +1,27 @@ +namespace AdsPush.Vapid +{ + /// + /// Returns from key generation request. + /// + /// + public class VapidKeyGenerationResult + { + public VapidKeyGenerationResult( + string publicLey, + string privateKey) + { + this.PublicLey = publicLey; + this.PrivateKey = privateKey; + } + + /// + /// Public key that's required for client operation. + /// + public string PublicLey { get; } + + /// + /// Private key that should be used in server-side encryption. + /// + public string PrivateKey { get; } + } +} diff --git a/src/AdsPush/AdsPushSender.cs b/src/AdsPush/AdsPushSender.cs index 9ae5540..00550cc 100644 --- a/src/AdsPush/AdsPushSender.cs +++ b/src/AdsPush/AdsPushSender.cs @@ -134,5 +134,11 @@ public IFirebasePushNotificationSender GetFirebaseSender() { return this._firebasePushNotificationSenderFactory.GetSender(this._appName); } + + /// + public IVapidPushNotificationSender GetVapidSender() + { + return this._vapidPushNotificationSenderFactory.GetSender(this._appName); + } } } diff --git a/src/AdsPush/IAdsPushSender.cs b/src/AdsPush/IAdsPushSender.cs index 6f5bad7..197c94a 100644 --- a/src/AdsPush/IAdsPushSender.cs +++ b/src/AdsPush/IAdsPushSender.cs @@ -1,8 +1,10 @@ +using System; using System.Threading; using System.Threading.Tasks; using AdsPush.Abstraction; using AdsPush.APNS; using AdsPush.Firebase; +using AdsPush.Vapid; namespace AdsPush { @@ -27,15 +29,21 @@ public interface IAdsPushSender CancellationToken cancellationToken = default); /// - /// Use to access whole platform specific options for APNS. + /// Use to access whole platform specific options for APNS. /// /// IApplePushNotificationSender GetApnsSender(); - + /// - /// Use to access whole platform specific options for Firebase. + /// Use to access whole platform specific options for Firebase. /// /// IFirebasePushNotificationSender GetFirebaseSender(); + + /// + /// Use to access whole platform specific options for Vapid. + /// + /// + IVapidPushNotificationSender GetVapidSender(); } } From d6dc7fc9d7196f35077e7041ff23f4c63afaadc7 Mon Sep 17 00:00:00 2001 From: "Anil.Senel" Date: Sun, 20 Aug 2023 20:38:30 +0300 Subject: [PATCH 5/6] ! --- README-NUGET.md | 106 +++++++++++++---- README.md | 107 ++++++++++++++---- .../appsettings.Development.json | 18 ++- samples/AdsPushSample.ConsoleApp/Program.cs | 105 +++++++++-------- .../Settings/AdsPushVapidSettings.cs | 5 - src/AdsPush.Firebase/AdsPush.Firebase.csproj | 4 +- src/AdsPush.Vapid/AdsPush.Vapid.csproj | 71 +++++++----- .../Extensions/BuilderExtensions.cs | 1 - .../Extensions/MappingExtensions.cs | 2 +- .../IVapidPushNotificationSenderFactory.cs | 1 - src/AdsPush.Vapid/Util/JwsSigner.cs | 18 ++- src/AdsPush.Vapid/Util/UrlBase64.cs | 8 +- src/AdsPush.Vapid/VapidHelper.cs | 4 +- .../VapidPushNotificationSender.cs | 3 +- .../VapidPushNotificationSenderFactory.cs | 2 +- src/AdsPush/AdsPush.csproj | 4 +- 16 files changed, 314 insertions(+), 145 deletions(-) diff --git a/README-NUGET.md b/README-NUGET.md index c5db7bb..bf10b35 100644 --- a/README-NUGET.md +++ b/README-NUGET.md @@ -90,6 +90,7 @@ using AdsPush.Extensions; } ``` + And put the following section in your in your `appsettings.[ENV].json` ``` @@ -101,7 +102,8 @@ And put the following section in your in your `appsettings.[ENV].json` "MyApp": { "TargetMappings": { "Ios": "Apns", - "Android": "FirebaseCloudMessaging" + "Android": "FirebaseCloudMessaging", + "BrowserAndPwa": "VapidWebPush" }, "Apns": { "P8PrivateKey": "", @@ -121,20 +123,26 @@ And put the following section in your in your `appsettings.[ENV].json` "AuthProviderX509CertUrl": "", "TokenUri": "", "ClientX509CertUrl": "" + }, + "Vapid": { + "PublicKey": "", + "PrivateKey": "", + "Subject": "" } } } ... } ``` + If you wish to use host/pod environment or any secret provider you can set the following environment variables. ``` -AdsPush__MyApp__Apns__AppBundleIdentifier= -AdsPush__MyApp__Apns__EnvironmentType= -AdsPush__MyApp__Apns__P8PrivateKey= -AdsPush__MyApp__Apns__P8PrivateKeyId=<10 digit p8 certificate id. Usually a part of a downloadable certificate filename> -AdsPush__MyApp__Apns__TeamId= +AdsPush__MyApp__Apns__AppBundleIdentifier= +AdsPush__MyApp__Apns__EnvironmentType= +AdsPush__MyApp__Apns__P8PrivateKey= +AdsPush__MyApp__Apns__P8PrivateKeyId=<10-digit p8 certificate id; often part of a downloadable certificate filename> +AdsPush__MyApp__Apns__TeamId=<10-digit Apple team id shown on the Apple Developer Membership Page> AdsPush__MyApp__FirebaseCloudMessaging__AuthProviderX509CertUrl= AdsPush__MyApp__FirebaseCloudMessaging__AuthUri= AdsPush__MyApp__FirebaseCloudMessaging__ClientEmail= @@ -146,7 +154,11 @@ AdsPush__MyApp__FirebaseCloudMessaging__ProjectId= AdsPush__MyApp__FirebaseCloudMessaging__Type= AdsPush__MyApp__TargetMappings__Android=FirebaseCloudMessaging +AdsPush__MyApp__TargetMappings__BrowserAndPwa=VapidWebPush AdsPush__MyApp__TargetMappings__Ios=Apns +AdsPush__MyApp__Vapid__PrivateKey= +AdsPush__MyApp__Vapid__PublicKey= +AdsPush__MyApp__Vapid__Subject= ``` @@ -183,9 +195,15 @@ var firebaseSettings = new AdsPushFirebaseSettings() //put your configurations hare. }; +var vapidSettings = new AdsPushVapidSettings() +{ + //put your configurations hare. +}; + var sender = builder .ConfigureApns(apnsSettings, null) .ConfigureFirebase(firebaseSettings, AdsPushTarget.Android) + .ConfigureVapid(vapidSettings, null) .BuildSender(); ``` @@ -196,21 +214,43 @@ When you obtain `IAdsPushSender` instance by using one the methods shown above, ```csharp -await sender.BasicSendAsync( - AdsPushTarget.Ios, - "79eb1b9e623bbca0d2b218f44a18d7b8ef59dac4da5baa9949c3e99a48eb259a", - new () + +var basicPayload = new AdsPushBasicSendPayload() +{ + Title = AdsPushText.CreateUsingString("test"), + Detail = AdsPushText.CreateUsingString("detail"), + Badge = 52, + Sound = "default", + Parameters = new Dictionary() { - Title = AdsPushText.CreateUsingString("test"), - Detail = AdsPushText.CreateUsingString("detail"), - Badge = 52, - Sound = "default", - Parameters = new Dictionary() { - {"pushParam1","value1"}, - {"pushParam2","value2"}, - } - }); + "pushParam1", "value1" + }, + { + "pushParam2", "value2" + }, + } +}; + +var apnDeviceToken = "15f6fdd0f34a7e0f46301a817536f0fb1b2ab05b09b3fae02beba2854a1a2a16"; +//var apnDeviceTokenVapid = "{"endpoint:"...", "keys": {"auth":"...","p256dh":"..."}}"; + +await sender.BasicSendAsync( + AdsPushTarget.Ios, + apnDeviceToken, + basicPayload); + +//For VAPID WebPush with multi parametere +string + endpoint = "https://fcm.googleapis.com/fcm/send/cIo6QJ4MMtQ:APA91bEGHCpZdHaUS7otb5_xU1zNWe6TAqria9phFm7M_9ZIiEyr0vXj3gRHbeIJMYvp2-SAVbgNrVvl7uBvU_VTLpIA0CLBcmqXuuEktGr0U4LVLvwWBibO68spJk7D-lr8R9zPyAXE", + p256dh = "BIjydse4Rij892SJN10xx1qbxDM6GrYXSfg7TGu90CVM1WmlTYzn_79psRqseyWdER969LGLjZmnXIhHPaKTyGE", + auth = "TkLGLzFeUU3C9SJJN6dLAA"; + +var subscription = VapidSubscription.FromParameters(endpoint, p256dh, auth); +await sender.BasicSendAsync( + AdsPushTarget.BrowserAndPwa, + subscription.ToAdsPushToken(), + basicPayload); ``` @@ -268,4 +308,32 @@ var firebaseResult = await sender ImageUrl = "" } }); + + + +//Sample for VAPID WebPush +var vapidResult = await sender + .GetVapidSender() + .SendAsync( + subscription, + new VapidRequest() + { + Title = "", + Badge = "", + Message = "", + Sound = "", + Icon = "", + Image = "", + Language = "", + Silent = false, + Tag = "", + ClickAction = "", + VibratePattern = "", + Data = new Dictionary() + { + {"param1", "value1"} + } + }); + + ``` diff --git a/README.md b/README.md index 30ccd71..9e1de81 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@

AdsPush

-AdsPush is the server-side push notification library that fully supports APNS (Apple Push Notification Service) and FCM (Firebase Cloud Messaging) features and works with the most common platforms. It offers good abstraction, is easy to use, and provides complete support for advanced use cases +AdsPush is the server-side push notification library that fully supports APNS (Apple Push Notification Service), FCM (Firebase Cloud Messaging) and VAPID WebPush features and works with the most common platforms. It offers good abstraction, is easy to use, and provides complete support for advanced use cases
Report Bug or Request Feature @@ -124,7 +124,8 @@ And put the following section in your in your `appsettings.[ENV].json` "MyApp": { "TargetMappings": { "Ios": "Apns", - "Android": "FirebaseCloudMessaging" + "Android": "FirebaseCloudMessaging", + "BrowserAndPwa": "VapidWebPush" }, "Apns": { "P8PrivateKey": "", @@ -144,20 +145,26 @@ And put the following section in your in your `appsettings.[ENV].json` "AuthProviderX509CertUrl": "", "TokenUri": "", "ClientX509CertUrl": "" + }, + "Vapid": { + "PublicKey": "", + "PrivateKey": "", + "Subject": "" } } } ... } ``` + If you wish to use host/pod environment or any secret provider you can set the following environment variables. ``` -AdsPush__MyApp__Apns__AppBundleIdentifier= -AdsPush__MyApp__Apns__EnvironmentType= -AdsPush__MyApp__Apns__P8PrivateKey= -AdsPush__MyApp__Apns__P8PrivateKeyId=<10 digit p8 certificate id. Usually a part of a downloadable certificate filename> -AdsPush__MyApp__Apns__TeamId= +AdsPush__MyApp__Apns__AppBundleIdentifier= +AdsPush__MyApp__Apns__EnvironmentType= +AdsPush__MyApp__Apns__P8PrivateKey= +AdsPush__MyApp__Apns__P8PrivateKeyId=<10-digit p8 certificate id; often part of a downloadable certificate filename> +AdsPush__MyApp__Apns__TeamId=<10-digit Apple team id shown on the Apple Developer Membership Page> AdsPush__MyApp__FirebaseCloudMessaging__AuthProviderX509CertUrl= AdsPush__MyApp__FirebaseCloudMessaging__AuthUri= AdsPush__MyApp__FirebaseCloudMessaging__ClientEmail= @@ -169,7 +176,11 @@ AdsPush__MyApp__FirebaseCloudMessaging__ProjectId= AdsPush__MyApp__FirebaseCloudMessaging__Type= AdsPush__MyApp__TargetMappings__Android=FirebaseCloudMessaging +AdsPush__MyApp__TargetMappings__BrowserAndPwa=VapidWebPush AdsPush__MyApp__TargetMappings__Ios=Apns +AdsPush__MyApp__Vapid__PrivateKey= +AdsPush__MyApp__Vapid__PublicKey= +AdsPush__MyApp__Vapid__Subject= ``` @@ -206,9 +217,15 @@ var firebaseSettings = new AdsPushFirebaseSettings() //put your configurations hare. }; +var vapidSettings = new AdsPushVapidSettings() +{ + //put your configurations hare. +}; + var sender = builder .ConfigureApns(apnsSettings, null) .ConfigureFirebase(firebaseSettings, AdsPushTarget.Android) + .ConfigureVapid(vapidSettings, null) .BuildSender(); ``` @@ -219,21 +236,43 @@ When you obtain `IAdsPushSender` instance by using one the methods shown above, ```csharp -await sender.BasicSendAsync( - AdsPushTarget.Ios, - "79eb1b9e623bbca0d2b218f44a18d7b8ef59dac4da5baa9949c3e99a48eb259a", - new () + +var basicPayload = new AdsPushBasicSendPayload() +{ + Title = AdsPushText.CreateUsingString("test"), + Detail = AdsPushText.CreateUsingString("detail"), + Badge = 52, + Sound = "default", + Parameters = new Dictionary() { - Title = AdsPushText.CreateUsingString("test"), - Detail = AdsPushText.CreateUsingString("detail"), - Badge = 52, - Sound = "default", - Parameters = new Dictionary() { - {"pushParam1","value1"}, - {"pushParam2","value2"}, - } - }); + "pushParam1", "value1" + }, + { + "pushParam2", "value2" + }, + } +}; + +var apnDeviceToken = "15f6fdd0f34a7e0f46301a817536f0fb1b2ab05b09b3fae02beba2854a1a2a16"; +//var apnDeviceTokenVapid = "{"endpoint:"...", "keys": {"auth":"...","p256dh":"..."}}"; + +await sender.BasicSendAsync( + AdsPushTarget.Ios, + apnDeviceToken, + basicPayload); + +//For VAPID WebPush with multi parametere +string + endpoint = "https://fcm.googleapis.com/fcm/send/cIo6QJ4MMtQ:APA91bEGHCpZdHaUS7otb5_xU1zNWe6TAqria9phFm7M_9ZIiEyr0vXj3gRHbeIJMYvp2-SAVbgNrVvl7uBvU_VTLpIA0CLBcmqXuuEktGr0U4LVLvwWBibO68spJk7D-lr8R9zPyAXE", + p256dh = "BIjydse4Rij892SJN10xx1qbxDM6GrYXSfg7TGu90CVM1WmlTYzn_79psRqseyWdER969LGLjZmnXIhHPaKTyGE", + auth = "TkLGLzFeUU3C9SJJN6dLAA"; + +var subscription = VapidSubscription.FromParameters(endpoint, p256dh, auth); +await sender.BasicSendAsync( + AdsPushTarget.BrowserAndPwa, + subscription.ToAdsPushToken(), + basicPayload); ``` @@ -291,6 +330,34 @@ var firebaseResult = await sender ImageUrl = "" } }); + + + +//Sample for VAPID WebPush +var vapidResult = await sender + .GetVapidSender() + .SendAsync( + subscription, + new VapidRequest() + { + Title = "", + Badge = "", + Message = "", + Sound = "", + Icon = "", + Image = "", + Language = "", + Silent = false, + Tag = "", + ClickAction = "", + VibratePattern = "", + Data = new Dictionary() + { + {"param1", "value1"} + } + }); + + ``` diff --git a/samples/AdsPushSample.Api/appsettings.Development.json b/samples/AdsPushSample.Api/appsettings.Development.json index 1c19a1e..cb766a9 100644 --- a/samples/AdsPushSample.Api/appsettings.Development.json +++ b/samples/AdsPushSample.Api/appsettings.Development.json @@ -9,14 +9,15 @@ "MyApp": { "TargetMappings": { "Ios": "Apns", - "Android": "FirebaseCloudMessaging" + "Android": "FirebaseCloudMessaging", + "BrowserAndPwa": "VapidWebPush" }, "Apns": { - "P8PrivateKey": "", - "P8PrivateKeyId": "<10 digit p8 certificate id. Usually a part of a downloadable certificate filename>", - "TeamId": "", - "AppBundleIdentifier": "", - "EnvironmentType": "" + "P8PrivateKey": "", + "P8PrivateKeyId": "<10-digit p8 certificate id; often part of a downloadable certificate filename>", + "TeamId": "<10-digit Apple team id shown on the Apple Developer Membership Page>", + "AppBundleIdentifier": "", + "EnvironmentType": "" }, "FirebaseCloudMessaging": { "Type":"", @@ -29,6 +30,11 @@ "AuthProviderX509CertUrl": "", "TokenUri": "", "ClientX509CertUrl": "" + }, + "Vapid": { + "PublicKey": "", + "PrivateKey": "", + "Subject": "" } } } diff --git a/samples/AdsPushSample.ConsoleApp/Program.cs b/samples/AdsPushSample.ConsoleApp/Program.cs index 023bd3a..6e1c32f 100644 --- a/samples/AdsPushSample.ConsoleApp/Program.cs +++ b/samples/AdsPushSample.ConsoleApp/Program.cs @@ -6,6 +6,7 @@ using AdsPush.Abstraction; using AdsPush.Abstraction.APNS; using AdsPush.Abstraction.Settings; +using AdsPush.Abstraction.Vapid; using AdsPush.Vapid; using FirebaseAdmin.Messaging; @@ -21,70 +22,54 @@ }; -var publicKey = "BF59A9jkMtVqs0Gzef1o6xhcB8SBHjhufCLikJhtNY9YGl_Zm2PwLMYbQs_RvD3T0yUFUlcFBt6nqSVOdoU05IM"; -var privateKey = "jYJABdhwbgAOiQkz97LK39FjA5YF4WXPxcgDX7bdRcQ"; -var subject = @"mailto:example@example.com"; var vapidSettings = new AdsPushVapidSettings() { //put your configurations hare. - PublicKey = publicKey, - PrivateKey = privateKey, - Subject = subject }; var sender = builder - .ConfigureVapid(vapidSettings, null) - .ConfigureApns(apnsSettings, null) + .ConfigureVapid(vapidSettings) + .ConfigureApns(apnsSettings) .ConfigureFirebase(firebaseSettings, AdsPushTarget.Android) .BuildSender(); -// string -// endpoint = "https://fcm.googleapis.com/fcm/send/cIo6QJ4MMtQ:APA91bEGHCpZdHaUS7otb5_xU1zNWe6TAqria9phFm7M_9ZIiEyr0vXj3gRHbeIJMYvp2-SAVbgNrVvl7uBvU_VTLpIA0CLBcmqXuuEktGr0U4LVLvwWBibO68spJk7D-lr8R9zPyAXE", -// p256dh = "BIjydse4Rij892SJN10xx1qbxDM6GrYXSfg7TGu90CVM1WmlTYzn_79psRqseyWdER969LGLjZmnXIhHPaKTyGE", -// auth = "TkLGLzFeUU3C9SJJN6dLAA"; - +var basicPayload = new AdsPushBasicSendPayload() +{ + Title = AdsPushText.CreateUsingString("test"), + Detail = AdsPushText.CreateUsingString("detail"), + Badge = 52, + Sound = "default", + Parameters = new Dictionary() + { + { + "pushParam1", "value1" + }, + { + "pushParam2", "value2" + }, + } +}; +var apnDeviceToken = "15f6fdd0f34a7e0f46301a817536f0fb1b2ab05b09b3fae02beba2854a1a2a16"; +//var apnDeviceTokenVapid = "{"endpoint:"...", "keys": {"auth":"...","p256dh":"..."}}"; +await sender.BasicSendAsync( + AdsPushTarget.Ios, + apnDeviceToken, + basicPayload); -//Safari mobile +//For VAPID WebPush string - endpoint = "", - p256dh = "", - auth = ""; + endpoint = "https://fcm.googleapis.com/fcm/send/cIo6QJ4MMtQ:APA91bEGHCpZdHaUS7otb5_xU1zNWe6TAqria9phFm7M_9ZIiEyr0vXj3gRHbeIJMYvp2-SAVbgNrVvl7uBvU_VTLpIA0CLBcmqXuuEktGr0U4LVLvwWBibO68spJk7D-lr8R9zPyAXE", + p256dh = "BIjydse4Rij892SJN10xx1qbxDM6GrYXSfg7TGu90CVM1WmlTYzn_79psRqseyWdER969LGLjZmnXIhHPaKTyGE", + auth = "TkLGLzFeUU3C9SJJN6dLAA"; - -var subs = VapidSubscription.FromParameters(endpoint, p256dh, auth); +var subscription = VapidSubscription.FromParameters(endpoint, p256dh, auth); await sender.BasicSendAsync( AdsPushTarget.BrowserAndPwa, - subs.ToAdsPushToken(), - new AdsPushBasicSendPayload() - { - Title = AdsPushText.CreateUsingString("Title"), - Detail = AdsPushText.CreateUsingString("Detail"), - - }); - -var apnDeviceToken = "15f6fdd0f34a7e0f46301a817536f0fb1b2ab05b09b3fae02beba2854a1a2a16"; + subscription.ToAdsPushToken(), + basicPayload); -await sender.BasicSendAsync( - AdsPushTarget.Ios, - apnDeviceToken, - new() - { - Title = AdsPushText.CreateUsingString("test"), - Detail = AdsPushText.CreateUsingString("detail"), - Badge = 52, - Sound = "default", - Parameters = new Dictionary() - { - { - "pushParam1", "value1" - }, - { - "pushParam2", "value2" - }, - } - }); //for whole platform options //sample for Apns @@ -142,3 +127,29 @@ ImageUrl = "" } }); + +//Sample for VAPID WebPush +var vapidResult = await sender + .GetVapidSender() + .SendAsync( + subscription, + new VapidRequest() + { + Title = "", + Badge = "", + Message = "", + Sound = "", + Icon = "", + Image = "", + Language = "", + Silent = false, + Tag = "", + ClickAction = "", + VibratePattern = "", + Data = new Dictionary() + { + { + "param1", "value1" + } + } + }); diff --git a/src/AdsPush.Abstraction/Settings/AdsPushVapidSettings.cs b/src/AdsPush.Abstraction/Settings/AdsPushVapidSettings.cs index 8874856..a0edb8c 100644 --- a/src/AdsPush.Abstraction/Settings/AdsPushVapidSettings.cs +++ b/src/AdsPush.Abstraction/Settings/AdsPushVapidSettings.cs @@ -16,10 +16,5 @@ public class AdsPushVapidSettings /// Gets or sets the subject for VAPID authentication. This should be a mailto or a URL. ///

public string Subject { get; set; } - - /// - /// Gets or sets the Time To Live (TTL) for the notification. This defines how long a push message is retained if the user's device is offline. If not delivered in this time, the message will be dropped. - /// - public long? TTL { get; set; } } } diff --git a/src/AdsPush.Firebase/AdsPush.Firebase.csproj b/src/AdsPush.Firebase/AdsPush.Firebase.csproj index 8eae929..d5f3c2b 100644 --- a/src/AdsPush.Firebase/AdsPush.Firebase.csproj +++ b/src/AdsPush.Firebase/AdsPush.Firebase.csproj @@ -4,9 +4,9 @@ netstandard2.0 AdsPush.Firebase Anil Dursun SENEL - push;APNS;service-side-push-library;Firebase;Apple;FCM;push notification + push;APNS;service-side-push-library;Firebase;Apple;FCM;VAPID;WebPush;push notification - AdsPush is the server-side push notification library that fully supports APNS(Apple Push Notification Service) and FCM (Firebase Cloud Messaging) features and works with the the most common .NET platorms. It puts togetter good abtraction, easy using and full support for advanced use cases. + AdsPush is the server-side push notification library that fully supports APNS(Apple Push Notification Service), FCM (Firebase Cloud Messaging) and VAPID WebPush features and works with the the most common .NET platorms. It puts togetter good abtraction, easy using and full support for advanced use cases. logo.png https://github.com/adessoTurkey-dotNET/AdsPush diff --git a/src/AdsPush.Vapid/AdsPush.Vapid.csproj b/src/AdsPush.Vapid/AdsPush.Vapid.csproj index c0eccd4..051d7cd 100644 --- a/src/AdsPush.Vapid/AdsPush.Vapid.csproj +++ b/src/AdsPush.Vapid/AdsPush.Vapid.csproj @@ -1,37 +1,50 @@ - - netstandard2.0 - + + netstandard2.0 + AdsPush.Vapid + Anil Dursun SENEL + push;APNS;server-side-push-library;Firebase;Apple;FCM;VAPID;WebPush;push notification + + AdsPush is the server-side push notification library that fully supports APNS(Apple Push Notification Service), FCM (Firebase Cloud Messaging) and VAPID WebPush features and works with the the most common .NET platorms. It puts togetter good abtraction, easy using and full support for advanced use cases. + + logo.png + https://github.com/adessoTurkey-dotNET/AdsPush + https://github.com/adessoTurkey-dotNET/AdsPush + LICENSE + Copyright (c) 2023, Anıl Dursun ŞENEL + README-NUGET.md + true + - - - + + + - - - true - / - LICENSE - - - true - / - logo.png - - - true - / - README-NUGET.md - - + + + true + / + LICENSE + + + true + / + logo.png + + + true + / + README-NUGET.md + + - - - - - - + + + + + + diff --git a/src/AdsPush.Vapid/Extensions/BuilderExtensions.cs b/src/AdsPush.Vapid/Extensions/BuilderExtensions.cs index e0ef482..8354259 100644 --- a/src/AdsPush.Vapid/Extensions/BuilderExtensions.cs +++ b/src/AdsPush.Vapid/Extensions/BuilderExtensions.cs @@ -8,7 +8,6 @@ namespace AdsPush.Vapid.Extensions { public static class BuilderExtensions { - /// /// Configures to be able to creates instance. /// diff --git a/src/AdsPush.Vapid/Extensions/MappingExtensions.cs b/src/AdsPush.Vapid/Extensions/MappingExtensions.cs index 3def5f7..5be185c 100644 --- a/src/AdsPush.Vapid/Extensions/MappingExtensions.cs +++ b/src/AdsPush.Vapid/Extensions/MappingExtensions.cs @@ -53,7 +53,7 @@ public static class MappingExtensions Message = payload.Detail.Text, Tag = payload.GroupId, Sound = payload.Sound, - Data = payload.Parameters.ToDictionary(x=>x.Key, x=>x.Value.ToString()), + Data = payload.Parameters.ToDictionary(x => x.Key, x => x.Value.ToString()), Silent = payload.PushType is AdsPushType.Background, TTL = (long?)payload.Ttl?.TotalSeconds }; diff --git a/src/AdsPush.Vapid/IVapidPushNotificationSenderFactory.cs b/src/AdsPush.Vapid/IVapidPushNotificationSenderFactory.cs index a6b8eb9..f2d9a86 100644 --- a/src/AdsPush.Vapid/IVapidPushNotificationSenderFactory.cs +++ b/src/AdsPush.Vapid/IVapidPushNotificationSenderFactory.cs @@ -2,7 +2,6 @@ namespace AdsPush.Vapid { - public interface IVapidPushNotificationSenderFactory { IVapidPushNotificationSender GetSender( diff --git a/src/AdsPush.Vapid/Util/JwsSigner.cs b/src/AdsPush.Vapid/Util/JwsSigner.cs index 51c4dbc..bb93305 100644 --- a/src/AdsPush.Vapid/Util/JwsSigner.cs +++ b/src/AdsPush.Vapid/Util/JwsSigner.cs @@ -13,7 +13,8 @@ internal class JwsSigner { private readonly ECPrivateKeyParameters _privateKey; - public JwsSigner(ECPrivateKeyParameters privateKey) + public JwsSigner( + ECPrivateKeyParameters privateKey) { this._privateKey = privateKey; } @@ -24,7 +25,9 @@ public JwsSigner(ECPrivateKeyParameters privateKey) /// /// /// - public string GenerateSignature(Dictionary header, Dictionary payload) + public string GenerateSignature( + Dictionary header, + Dictionary payload) { var securedInput = SecureInput(header, payload); var message = Encoding.UTF8.GetBytes(securedInput); @@ -51,7 +54,9 @@ public string GenerateSignature(Dictionary header, Dictionary header, Dictionary payload) + private static string SecureInput( + Dictionary header, + Dictionary payload) { var encodeHeader = UrlBase64.Encode(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(header))); var encodePayload = UrlBase64.Encode(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload))); @@ -59,7 +64,9 @@ private static string SecureInput(Dictionary header, Dictionary< return $"{encodeHeader}.{encodePayload}"; } - private static byte[] ByteArrayPadLeft(byte[] src, int size) + private static byte[] ByteArrayPadLeft( + byte[] src, + int size) { var dst = new byte[size]; var startAt = dst.Length - src.Length; @@ -67,7 +74,8 @@ private static byte[] ByteArrayPadLeft(byte[] src, int size) return dst; } - private static byte[] Sha256Hash(byte[] message) + private static byte[] Sha256Hash( + byte[] message) { var sha256Digest = new Sha256Digest(); sha256Digest.BlockUpdate(message, 0, message.Length); diff --git a/src/AdsPush.Vapid/Util/UrlBase64.cs b/src/AdsPush.Vapid/Util/UrlBase64.cs index c610ec4..0872217 100644 --- a/src/AdsPush.Vapid/Util/UrlBase64.cs +++ b/src/AdsPush.Vapid/Util/UrlBase64.cs @@ -9,7 +9,8 @@ internal static class UrlBase64 /// /// /// - public static byte[] Decode(string base64) + public static byte[] Decode( + string base64) { base64 = base64.Replace('-', '+').Replace('_', '/'); @@ -26,9 +27,10 @@ public static byte[] Decode(string base64) /// /// /// - public static string Encode(byte[] data) + public static string Encode( + byte[] data) { return Convert.ToBase64String(data).Replace('+', '-').Replace('/', '_').TrimEnd('='); } } -} \ No newline at end of file +} diff --git a/src/AdsPush.Vapid/VapidHelper.cs b/src/AdsPush.Vapid/VapidHelper.cs index 867755b..fbcdd10 100644 --- a/src/AdsPush.Vapid/VapidHelper.cs +++ b/src/AdsPush.Vapid/VapidHelper.cs @@ -13,8 +13,8 @@ public static class VapidHelper public static VapidKeyGenerationResult GenerateVapidKeys() { var keys = ECKeyHelper.GenerateKeys(); - var publicKey = ((ECPublicKeyParameters) keys.Public).Q.GetEncoded(false); - var privateKey = ((ECPrivateKeyParameters) keys.Private).D.ToByteArrayUnsigned(); + var publicKey = ((ECPublicKeyParameters)keys.Public).Q.GetEncoded(false); + var privateKey = ((ECPrivateKeyParameters)keys.Private).D.ToByteArrayUnsigned(); return new VapidKeyGenerationResult( UrlBase64.Encode(publicKey), diff --git a/src/AdsPush.Vapid/VapidPushNotificationSender.cs b/src/AdsPush.Vapid/VapidPushNotificationSender.cs index 01a5537..8acf50d 100644 --- a/src/AdsPush.Vapid/VapidPushNotificationSender.cs +++ b/src/AdsPush.Vapid/VapidPushNotificationSender.cs @@ -17,6 +17,7 @@ namespace AdsPush.Vapid { public class VapidPushNotificationSender : IVapidPushNotificationSender { + private const long DefaultTtl = 43200; private readonly HttpClient _client; private readonly AdsPushVapidSettings _adsPushVapidSettings; @@ -145,7 +146,7 @@ public class VapidPushNotificationSender : IVapidPushNotificationSender return ttlLong; } - return this._adsPushVapidSettings.TTL.GetValueOrDefault(43200); + return DefaultTtl; } private bool ValidateSubscription( diff --git a/src/AdsPush.Vapid/VapidPushNotificationSenderFactory.cs b/src/AdsPush.Vapid/VapidPushNotificationSenderFactory.cs index e142e4f..ca11bbc 100644 --- a/src/AdsPush.Vapid/VapidPushNotificationSenderFactory.cs +++ b/src/AdsPush.Vapid/VapidPushNotificationSenderFactory.cs @@ -6,7 +6,7 @@ namespace AdsPush.Vapid { - public class VapidPushNotificationSenderFactory: IVapidPushNotificationSenderFactory + public class VapidPushNotificationSenderFactory : IVapidPushNotificationSenderFactory { public VapidPushNotificationSenderFactory( VapidSettingsSection vapidSettingsSection, diff --git a/src/AdsPush/AdsPush.csproj b/src/AdsPush/AdsPush.csproj index 8dfa5ff..a154ba9 100644 --- a/src/AdsPush/AdsPush.csproj +++ b/src/AdsPush/AdsPush.csproj @@ -5,9 +5,9 @@ netstandard2.0 AdsPush Anil Dursun SENEL - push;APNS;server-side-push-library;Firebase;Apple;FCM;ios-push;android-push;notofication;push-notification;push notification + push;APNS;server-side-push-library;Firebase;Apple;FCM;ios-push;android-push;notofication;VAPID;WebPush;push-notification;push notification - AdsPush is the server-side push notification library that fully supports APNS(Apple Push Notification Service) and FCM (Firebase Cloud Messaging) features and works with the the most common .NET platorms. It puts togetter good abtraction, easy using and full support for advanced use cases. + AdsPush is the server-side push notification library that fully supports APNS(Apple Push Notification Service), FCM (Firebase Cloud Messaging) and VAPID WebPush features and works with the the most common .NET platorms. It puts togetter good abtraction, easy using and full support for advanced use cases. true logo.png From 22d8a122320e3e5b873963e69be4da424e298eed Mon Sep 17 00:00:00 2001 From: "Anil.Senel" Date: Sun, 20 Aug 2023 20:42:44 +0300 Subject: [PATCH 6/6] . --- .../service-worker.js | 55 ++++++++++--------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/samples/AdsPushSample.VapidClient/service-worker.js b/samples/AdsPushSample.VapidClient/service-worker.js index 3c3dd50..8b7a79e 100644 --- a/samples/AdsPushSample.VapidClient/service-worker.js +++ b/samples/AdsPushSample.VapidClient/service-worker.js @@ -1,39 +1,44 @@ self.addEventListener("push", (event) => { - if (!(self.Notification && self.Notification.permission === "granted")) { - return; - } + if (!(self.Notification && self.Notification.permission === "granted")) { + return; + } + + const data = event.data?.json() ?? {}; + const icon = "icon.png"; - const data = event.data?.json() ?? {}; - const icon = "icon.png"; + const options = { + lang: data.lang || "en-US", + title: data.title, + body: data.message, + tag: data.tag, + silent: data.silent, + image: data.image, + vibrate: [200, 100, 200], + actions: data.actions || [], + icon, + data: { + url: data.click_action + } + }; - const options = { - lang: data.lang || "en-US", - title: data.title, - body: data.message, - tag: data.tag, - image: data.image, - vibrate: [200, 100, 200], - actions: data.actions || [], - icon, - data: { - url: data.click_action // Bu veriyi tıklandığında kullanıyoruz - } - }; + //do your operations + if (options.silent) + return; + event.waitUntil(self.registration.showNotification(data.title, options)); - event.waitUntil(self.registration.showNotification(data.title, options)); }); self.addEventListener('notificationclick', function (event) { - event.notification.close(); // Bildirimi kapat - if (clients.openWindow && event.notification.data.url) { - event.waitUntil(clients.openWindow(event.notification.data.url)); - } + event.notification.close(); // Bildirimi kapat + if (clients.openWindow && event.notification.data.url) { + event.waitUntil(clients.openWindow(event.notification.data.url)); + } }); self.addEventListener('install', function (event) { - self.skipWaiting(); // Hemen yüklenmesini sağla + self.skipWaiting(); }); self.addEventListener('activate', function (event) { - event.waitUntil(clients.claim()); // Hemen etkinleşmesini sağla + event.waitUntil(clients.claim()); // Hemen etkinleşmesini sağla });