diff --git a/Examples/VapidHeaders.cs b/Examples/VapidHeaders.cs new file mode 100644 index 0000000..c651e16 --- /dev/null +++ b/Examples/VapidHeaders.cs @@ -0,0 +1,22 @@ +#!/usr/bin/env dotnet +#:package ClosureOSS.WebPush@2.4.1 +using WebPush; + +var uri = new Uri("https://server.example.com/notify"); +var audience = uri.Scheme + Uri.SchemeDelimiter + uri.Host; +var vapidKeys = VapidHelper.GenerateVapidKeys(); + + +var headers = VapidHelper.GetVapidHeaders( + audience, + @"mailto: example@example.com", + vapidKeys.PublicKey, + vapidKeys.PrivateKey, + DateTime.Now.AddDays(2), + ContentEncoding.Aes128gcm +); + +foreach (var header in headers) +{ + Console.WriteLine($"{header.Key}: {header.Value}"); +} diff --git a/Examples/VapidKeys.cs b/Examples/VapidKeys.cs new file mode 100644 index 0000000..10ed000 --- /dev/null +++ b/Examples/VapidKeys.cs @@ -0,0 +1,8 @@ +#!/usr/bin/env dotnet +#:package ClosureOSS.WebPush@2.4.1 +using WebPush; + +var vapidKeys = VapidHelper.GenerateVapidKeys(); +Console.WriteLine($"Public key: {vapidKeys.PublicKey}"); +Console.WriteLine($"Private key: {vapidKeys.PrivateKey}"); + diff --git a/Examples/WebPushMessage.cs b/Examples/WebPushMessage.cs new file mode 100644 index 0000000..4aecb18 --- /dev/null +++ b/Examples/WebPushMessage.cs @@ -0,0 +1,30 @@ +#!/usr/bin/env dotnet +#:package ClosureOSS.WebPush@2.4.1 +using WebPush; + +var vapidKeys = VapidHelper.GenerateVapidKeys(); +vapidKeys.Subject = @"mailto:user@example.net"; +// example keys +var p256dh = @"BIwCq8tz028CFq9YFQ56kipZ633EK628l4-u6FcPHTGkS_cqTsV-9MRRAGEu56UmfXQ-8lIg7QXgUTFmzedzMHM"; +var auth = @"eSZCfw7d6bXE0erlgnLe_Q"; +var webPushClient = new WebPushClient(); +var subscription = new PushSubscription("https://server.example.com/notify/Avv3mSO...", p256dh, auth); +var options = new WebPushOptions +{ + VapidDetails = vapidKeys, + Topic = "Example", +}; + + +var message = webPushClient.GenerateRequestDetails(subscription, "A payload", options); + +Console.WriteLine($"{message.Method} {message.RequestUri}"); +Console.WriteLine(); +foreach (var header in message.Headers){ + foreach (var value in header.Value) + { + Console.WriteLine($"{header.Key} : {value}"); + } +} +Console.WriteLine(); +Console.WriteLine($"{Convert.ToBase64String(await message.Content!.ReadAsByteArrayAsync())}"); diff --git a/Examples/packages.lock.json b/Examples/packages.lock.json new file mode 100644 index 0000000..a57656f --- /dev/null +++ b/Examples/packages.lock.json @@ -0,0 +1,95 @@ +{ + "version": 1, + "dependencies": { + "net10.0": { + "ClosureOSS.WebPush": { + "type": "Direct", + "requested": "[2.4.1, )", + "resolved": "2.4.1", + "contentHash": "iraAOpOOyz+MafRKEBYPQuq7uCKAS3du4o+bK8SUFCi5C6pm1UCh/Yl4N2vDmZojM2875/QQ18oUWARBxaopWQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.IdentityModel.JsonWebTokens": "8.16.0", + "Microsoft.IdentityModel.Tokens": "8.16.0" + } + }, + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[10.0.7, )", + "resolved": "10.0.7", + "contentHash": "2H7j1NltkQx04sPWBkUtFrZNBtro7vwsxRtdThP0oDj6Sn3ouGHCQlxATZ4Me2aJE67+KiXMX2V1IHDjt1uIpw==" + }, + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[10.0.7, )", + "resolved": "10.0.7", + "contentHash": "AA/yhzFHNtQZXLdqjzujPy25G8EWwGWsAnxOE2zYSBoT/8QHP6ketN3CToD3DFreO653ipUwnKHo22B8AlBMCw==" + }, + "Nerdbank.GitVersioning": { + "type": "Direct", + "requested": "[3.9.50, )", + "resolved": "3.9.50", + "contentHash": "HtOgGF6jZ+WYbXnCUCYPT8Y2d6mIJo9ozjK/FINTRsXdm4Zgv9GehUMa7EFoGQkqrMcDJNOIDwCmENnvXg4UbA==" + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "SIe9zlVQJecnk/DTmevIcl6+aEDYhoVLc2eG2AKwVeNEC8CSyxHAbh4lf0xtHq9JUum0vVTEByGNTK+b6oihTQ==" + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "PDMMt7fvBatv6hcxxyJtXIzSwn7Dy00W6I2vDAOTYrQqNM2dF5A2L9n0uMzdPz2IPoNZWkAmYjoOCEdDLq0i4w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.16.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.16.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Microsoft.IdentityModel.Logging": "8.16.0" + } + } + }, + "net10.0/win-x64": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[10.0.7, )", + "resolved": "10.0.7", + "contentHash": "2H7j1NltkQx04sPWBkUtFrZNBtro7vwsxRtdThP0oDj6Sn3ouGHCQlxATZ4Me2aJE67+KiXMX2V1IHDjt1uIpw==", + "dependencies": { + "runtime.win-x64.Microsoft.DotNet.ILCompiler": "10.0.7" + } + }, + "runtime.win-x64.Microsoft.DotNet.ILCompiler": { + "type": "Transitive", + "resolved": "10.0.7", + "contentHash": "pdlgAPDgcAMCi1XMrgKTPcovBzM0IG9j8LbsIyXS8XXXIrPRyygByqJPJnPtTPDIHxXsLmd3tlMEDULglbBdKA==" + } + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 6581aa1..d03201e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Build](https://github.com/closureOSS/webpush-csharp/actions/workflows/dotnet.yml/badge.svg)](https://github.com/closureOSS/webpush-csharp/actions/workflows/dotnet.yml) ![NuGet Version](https://img.shields.io/nuget/v/ClosureOSS.WebPush) - # Why To deliver generic events using HTTP Push as outlined in [Generic Event Delivery Using HTTP](https://datatracker.ietf.org/doc/html/rfc8030), backend-triggered push messages must be encrypted. This is accomplished using the [Message Encryption for Web Push](https://datatracker.ietf.org/doc/html/rfc8291) standard, which relies on [Voluntary Application Server Identification (VAPID) for Web Push (RFC8292)](https://datatracker.ietf.org/doc/html/rfc8292) for authentication. Furthermore, any data included with the push message must be separately encrypted following the rules of [Encrypted Content-Encoding for HTTP (RFC8188)](https://datatracker.ietf.org/doc/html/rfc8188). @@ -12,6 +11,8 @@ This package makes it easy to send push notifications from an application server ## Purpose of fork +Support for message topic and message urgency flag. + Support for the "aes128gcm" HTTP Content Coding. Rewrite using System.Security.Crytography, Microsoft.IdentityModel and other first party interfaces. @@ -43,12 +44,17 @@ var publicKey = @"BDjASz8kkVBQJgWcD05uX3VxIs_gSHyuS023jnBoHBgUbg8zIJvTSQytR8MP4Z var privateKey = @"mryM-krWj_6IsIMGsd8wNFXGBxnx..............."; var subscription = new PushSubscription(pushEndpoint, p256dh, auth); -var vapidDetails = new VapidDetails(subject, publicKey, privateKey); +var options = new WebPushOptions +{ + VapidDetails = new VapidDetails(subject, publicKey, privateKey), + ContentEncoding = ContentEncoding.Aes128gcm, + Urgency = Urgency.High, +}; var webPushClient = new WebPushClient(); try { - await webPushClient.SendNotificationAsync(subscription, "payload", vapidDetails); + await webPushClient.SendNotificationAsync(subscription, "payload", options); } catch (WebPushException exception) { @@ -114,14 +120,43 @@ Options is an optional argument that if defined should be an Dictionary +## GenerateRequestDetails(pushSubscription, payload, options) + +Generates a Http message without sending. Parameters have the same meaning as with `SendNotificationAsync`. + +See [standalone example](Examples/WebPushMessage.cs). + +```csharp +var vapidKeys = VapidHelper.GenerateVapidKeys(); +vapidKeys.Subject = @"mailto:user@example.net"; +var p256dh = @"BI...MHM"; +var auth = @"eSZ...Q"; +var webPushClient = new WebPushClient(); +var subscription = new PushSubscription("https://server.example.com/notify/Avv3mSO...", p256dh, auth); +var options = new WebPushOptions +{ + VapidDetails = vapidKeys, + Topic = "Example", +}; +``` + +
+ ## GenerateVapidKeys() +See [standalone example](Examples/VapidKeys.cs). + ```csharp var vapidKeys = VapidHelper.GenerateVapidKeys(); +Console.WriteLine($"Public key: {vapidKeys.PublicKey}"); +Console.WriteLine($"Private key: {vapidKeys.PrivateKey}"); +``` + +outputs for example: -// Prints 2 URL Safe Base64 Encoded Strings -Console.WriteLine("Public {0}", vapidKeys.PublicKey); -Console.WriteLine("Private {0}", vapidKeys.PrivateKey); +```text +Public key: BFu5Jx7eA285mMZRx7a-SuFH8Cc2mAMZ5RhbqvGJKAIqRT6VzRc4Y5x7uuBD2AVkeLn13MrQZKHHUV6QDL8arGM +Private key: 5aRRAbKkELlCDlhEO68GItWm9ux2hS7ORP2KmQVHxAI ``` ### Input @@ -139,6 +174,8 @@ Returns a VapidDetails object with **PublicKey** and **PrivateKey** values popul ## GetVapidHeaders(audience, subject, publicKey, privateKey, expiration) +See [standalone example](Examples/VapidHeaders.cs). + ```csharp Uri uri = new Uri(subscription.Endpoint); string audience = uri.Scheme + Uri.SchemeDelimiter + uri.Host; @@ -147,7 +184,9 @@ Dictionary vapidHeaders = VapidHelper.GetVapidHeaders( audience, @"mailto: example@example.com", publicKey, - privateKey + privateKey, + DateTime.Now.AddDays(2), + ContentEncoding.Aes128gcm ); ``` @@ -162,11 +201,14 @@ The `GetVapidHeaders()` method expects the following input: - _publicKey_: the VAPID public key. - _privateKey_: the VAPID private key. -### Returns +and optionally + +- _expiration_: Expiration date (defaults to 12 hours from now) +- _contentEncoding_: Either Aes128gcm (default) or Aesgcm -This method returns a Dictionary intented to be headers of a web request. It will contain the following key(s): +### Returns -- _Authorization_ +This method returns a Dictionary intented to be headers of a web request. It will contain the `Authorization` header and for Aes128 additionally a `Crypto-Key` header. --- diff --git a/WebPush.sln b/WebPush.sln deleted file mode 100755 index 9b7a190..0000000 --- a/WebPush.sln +++ /dev/null @@ -1,31 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2020 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebPush", "WebPush\WebPush.csproj", "{2003DA6A-AB53-4B2D-9ECC-846B7E3E3D5E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebPush.Test", "WebPush.Test\WebPush.Test.csproj", "{1CA4B246-C3C1-43F0-818C-8CBC994E1225}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2003DA6A-AB53-4B2D-9ECC-846B7E3E3D5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2003DA6A-AB53-4B2D-9ECC-846B7E3E3D5E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2003DA6A-AB53-4B2D-9ECC-846B7E3E3D5E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2003DA6A-AB53-4B2D-9ECC-846B7E3E3D5E}.Release|Any CPU.Build.0 = Release|Any CPU - {1CA4B246-C3C1-43F0-818C-8CBC994E1225}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1CA4B246-C3C1-43F0-818C-8CBC994E1225}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1CA4B246-C3C1-43F0-818C-8CBC994E1225}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1CA4B246-C3C1-43F0-818C-8CBC994E1225}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {2524F24D-BE69-493E-9563-448AAD5EDE30} - EndGlobalSection -EndGlobal diff --git a/WebPush.slnx b/WebPush.slnx new file mode 100644 index 0000000..caaa4f2 --- /dev/null +++ b/WebPush.slnx @@ -0,0 +1,4 @@ + + + +