├── .github
├── FUNDING.yml
└── workflows
│ ├── cd-aspnet-extensions.yml
│ ├── cd-azure-functions-worker-extensions.yml
│ ├── cd-azure-webjobs-extensions.yml
│ ├── cd-net-http.yml
│ ├── ci.yml
│ └── gh-pages.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.md
├── Lib.Net.Http.WebPush.sln
├── README.md
├── benchmarks
└── Benchmark.Net.Http.WebPush
│ ├── Benchmark.Net.Http.WebPush.csproj
│ ├── Program.cs
│ └── WebPushBenchmarks.cs
├── docs
└── DocFx.Net.Http.WebPush
│ ├── DocFx.Net.Http.WebPush.csproj
│ ├── Program.cs
│ ├── Startup.cs
│ ├── articles
│ ├── aspnetcore-integration.md
│ ├── azure-functions-integration.md
│ └── performance-considerations.md
│ ├── docfx.json
│ ├── index.md
│ ├── resources
│ ├── ico
│ │ └── favicon.ico
│ └── svg
│ │ └── logo.svg
│ └── toc.md
├── src
├── Lib.AspNetCore.WebPush
│ ├── Caching
│ │ ├── DistributedVapidTokenCache.cs
│ │ └── MemoryVapidTokenCache.cs
│ ├── Lib.AspNetCore.WebPush.csproj
│ ├── Lib.AspNetCore.WebPush.snk
│ ├── PushServiceClientOptions.cs
│ └── PushServiceClientServiceCollectionExtensions.cs
├── Lib.Azure.Functions.Worker.Extensions.WebPush
│ ├── Constants.cs
│ ├── FunctionsWorkerApplicationBuilderExtensions.cs
│ ├── Lib.Azure.Functions.Worker.Extensions.WebPush.csproj
│ ├── Properties
│ │ └── AssemblyInfo.cs
│ ├── PushServiceConverter.cs
│ ├── PushServiceExtensionStartup.cs
│ ├── PushServiceInputAttribute.cs
│ └── PushServiceModelBindingDataContent.cs
├── Lib.Azure.WebJobs.Extensions.WebPush
│ ├── Bindings
│ │ ├── PushServiceAttribute.cs
│ │ ├── PushServiceClientConverter.cs
│ │ └── PushServiceParameterBindingDataContent.cs
│ ├── Config
│ │ └── PushServiceExtensionConfigProvider.cs
│ ├── Constants.cs
│ ├── Lib.Azure.WebJobs.Extensions.WebPush.csproj
│ ├── Lib.Azure.WebJobs.Extensions.WebPush.snk
│ ├── PushServiceOptions.cs
│ ├── PushServiceWebJobsBuilderExtensions.cs
│ └── PushServiceWebJobsStartup.cs
└── Lib.Net.Http.WebPush
│ ├── Authentication
│ ├── IVapidTokenCache.cs
│ ├── VapidAuthentication.cs
│ └── VapidAuthenticationScheme.cs
│ ├── Internals
│ ├── ECDHAgreement.cs
│ ├── ECDHAgreementCalculator.BouncyCastle.cs
│ ├── ECDHAgreementCalculator.NET.cs
│ ├── ES256Signer.BouncyCastle.cs
│ ├── ES256Signer.NET.cs
│ └── UrlBase64Converter.cs
│ ├── Lib.Net.Http.WebPush.csproj
│ ├── Lib.Net.Http.WebPush.snk
│ ├── PushEncryptionKeyName.cs
│ ├── PushMessage.cs
│ ├── PushMessageUrgency.cs
│ ├── PushServiceClient.cs
│ ├── PushServiceClientException.cs
│ └── PushSubscription.cs
└── test
└── Test.Lib.Net.Http.WebPush
├── Functional
├── Infrastructure
│ ├── FakePushServiceApplicationFactory.cs
│ └── FakePushServiceStartup.cs
└── PushMessageDeliveryTests.cs
├── Test.Lib.Net.Http.WebPush.csproj
└── Unit
└── PushSubscriptionTests.cs
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: tpeczek
2 |
--------------------------------------------------------------------------------
/.github/workflows/cd-aspnet-extensions.yml:
--------------------------------------------------------------------------------
1 | name: Lib.AspNetCore.WebPush - CD
2 | on:
3 | push:
4 | tags:
5 | - "aspnet-extensions-v[0-9]+.[0-9]+.[0-9]+"
6 | jobs:
7 | deployment:
8 | runs-on: windows-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v4
12 | - name: Extract VERSION
13 | run: echo "VERSION=${GITHUB_REF/refs\/tags\/aspnet-extensions-v/}" >> $GITHUB_ENV
14 | shell: bash
15 | - name: Setup .NET Core 3.1 SDK
16 | uses: actions/setup-dotnet@v4
17 | with:
18 | dotnet-version: '3.1.x'
19 | - name: Setup .NET 5.0 SDK
20 | uses: actions/setup-dotnet@v4
21 | with:
22 | dotnet-version: '5.0.x'
23 | - name: Setup .NET 6.0 SDK
24 | uses: actions/setup-dotnet@v4
25 | with:
26 | dotnet-version: '6.0.x'
27 | - name: Setup .NET 8.0 SDK
28 | uses: actions/setup-dotnet@v4
29 | with:
30 | dotnet-version: '8.0.x'
31 | - name: Setup .NET 9.0 SDK
32 | uses: actions/setup-dotnet@v4
33 | with:
34 | dotnet-version: '9.0.x'
35 | - name: Restore
36 | run: dotnet restore
37 | - name: Build
38 | run: dotnet build --configuration Release --no-restore
39 | - name: Test
40 | run: dotnet test --configuration Release --no-build
41 | - name: Pack
42 | run: dotnet pack --configuration Release --no-build
43 | - name: NuGet Push Lib.AspNetCore.WebPush
44 | run: dotnet nuget push src/Lib.AspNetCore.WebPush/bin/Release/Lib.AspNetCore.WebPush.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_API_KEY}
45 | shell: bash
46 | env:
47 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
--------------------------------------------------------------------------------
/.github/workflows/cd-azure-functions-worker-extensions.yml:
--------------------------------------------------------------------------------
1 | name: Lib.Azure.Functions.Worker.Extensions.WebPush - CD
2 | on:
3 | push:
4 | tags:
5 | - "azure-functions-worker-extensions-v[0-9]+.[0-9]+.[0-9]+"
6 | jobs:
7 | deployment:
8 | runs-on: windows-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v4
12 | - name: Extract VERSION
13 | run: echo "VERSION=${GITHUB_REF/refs\/tags\/azure-functions-worker-extensions-v/}" >> $GITHUB_ENV
14 | shell: bash
15 | - name: Setup .NET Core 3.1 SDK
16 | uses: actions/setup-dotnet@v4
17 | with:
18 | dotnet-version: '3.1.x'
19 | - name: Setup .NET 5.0 SDK
20 | uses: actions/setup-dotnet@v4
21 | with:
22 | dotnet-version: '5.0.x'
23 | - name: Setup .NET 6.0 SDK
24 | uses: actions/setup-dotnet@v4
25 | with:
26 | dotnet-version: '6.0.x'
27 | - name: Setup .NET 8.0 SDK
28 | uses: actions/setup-dotnet@v4
29 | with:
30 | dotnet-version: '8.0.x'
31 | - name: Setup .NET 9.0 SDK
32 | uses: actions/setup-dotnet@v4
33 | with:
34 | dotnet-version: '9.0.x'
35 | - name: Restore
36 | run: dotnet restore
37 | - name: Build
38 | run: dotnet build --configuration Release --no-restore
39 | - name: Test
40 | run: dotnet test --configuration Release --no-build
41 | - name: Pack
42 | run: dotnet pack --configuration Release --no-build
43 | - name: NuGet Push Lib.Azure.Functions.Worker.Extensions.WebPush
44 | run: dotnet nuget push src/Lib.Azure.Functions.Worker.Extensions.WebPush/bin/Release/Lib.Azure.Functions.Worker.Extensions.WebPush.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_API_KEY}
45 | shell: bash
46 | env:
47 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
--------------------------------------------------------------------------------
/.github/workflows/cd-azure-webjobs-extensions.yml:
--------------------------------------------------------------------------------
1 | name: Lib.Azure.WebJobs.Extensions.WebPush - CD
2 | on:
3 | push:
4 | tags:
5 | - "azure-webjobs-extensions-v[0-9]+.[0-9]+.[0-9]+"
6 | jobs:
7 | deployment:
8 | runs-on: windows-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v4
12 | - name: Extract VERSION
13 | run: echo "VERSION=${GITHUB_REF/refs\/tags\/azure-webjobs-extensions-v/}" >> $GITHUB_ENV
14 | shell: bash
15 | - name: Setup .NET Core 3.1 SDK
16 | uses: actions/setup-dotnet@v4
17 | with:
18 | dotnet-version: '3.1.x'
19 | - name: Setup .NET 5.0 SDK
20 | uses: actions/setup-dotnet@v4
21 | with:
22 | dotnet-version: '5.0.x'
23 | - name: Setup .NET 6.0 SDK
24 | uses: actions/setup-dotnet@v4
25 | with:
26 | dotnet-version: '6.0.x'
27 | - name: Setup .NET 8.0 SDK
28 | uses: actions/setup-dotnet@v4
29 | with:
30 | dotnet-version: '8.0.x'
31 | - name: Setup .NET 9.0 SDK
32 | uses: actions/setup-dotnet@v4
33 | with:
34 | dotnet-version: '9.0.x'
35 | - name: Restore
36 | run: dotnet restore
37 | - name: Build
38 | run: dotnet build --configuration Release --no-restore
39 | - name: Test
40 | run: dotnet test --configuration Release --no-build
41 | - name: Pack
42 | run: dotnet pack --configuration Release --no-build
43 | - name: NuGet Push Lib.Azure.WebJobs.Extensions.WebPush
44 | run: dotnet nuget push src/Lib.Azure.WebJobs.Extensions.WebPush/bin/Release/Lib.Azure.WebJobs.Extensions.WebPush.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_API_KEY}
45 | shell: bash
46 | env:
47 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
--------------------------------------------------------------------------------
/.github/workflows/cd-net-http.yml:
--------------------------------------------------------------------------------
1 | name: Lib.Net.Http.WebPush - CD
2 | on:
3 | push:
4 | tags:
5 | - "v[0-9]+.[0-9]+.[0-9]+"
6 | jobs:
7 | deployment:
8 | runs-on: windows-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v4
12 | - name: Extract VERSION
13 | run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV
14 | shell: bash
15 | - name: Setup .NET Core 3.1 SDK
16 | uses: actions/setup-dotnet@v4
17 | with:
18 | dotnet-version: '3.1.x'
19 | - name: Setup .NET 5.0 SDK
20 | uses: actions/setup-dotnet@v4
21 | with:
22 | dotnet-version: '5.0.x'
23 | - name: Setup .NET 6.0 SDK
24 | uses: actions/setup-dotnet@v4
25 | with:
26 | dotnet-version: '6.0.x'
27 | - name: Setup .NET 8.0 SDK
28 | uses: actions/setup-dotnet@v4
29 | with:
30 | dotnet-version: '8.0.x'
31 | - name: Setup .NET 9.0 SDK
32 | uses: actions/setup-dotnet@v4
33 | with:
34 | dotnet-version: '9.0.x'
35 | - name: Restore
36 | run: dotnet restore
37 | - name: Build
38 | run: dotnet build --configuration Release --no-restore
39 | - name: Test
40 | run: dotnet test --configuration Release --no-build
41 | - name: Pack
42 | run: dotnet pack --configuration Release --no-build
43 | - name: NuGet Push Lib.Net.Http.WebPush
44 | run: dotnet nuget push src/Lib.Net.Http.WebPush/bin/Release/Lib.Net.Http.WebPush.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_API_KEY}
45 | shell: bash
46 | env:
47 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | jobs:
10 | integration:
11 | runs-on: windows-latest
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v4
15 | - name: Setup .NET Core 3.1 SDK
16 | uses: actions/setup-dotnet@v4
17 | with:
18 | dotnet-version: '3.1.x'
19 | - name: Setup .NET 5.0 SDK
20 | uses: actions/setup-dotnet@v4
21 | with:
22 | dotnet-version: '5.0.x'
23 | - name: Setup .NET 6.0 SDK
24 | uses: actions/setup-dotnet@v4
25 | with:
26 | dotnet-version: '6.0.x'
27 | - name: Setup .NET 8.0 SDK
28 | uses: actions/setup-dotnet@v4
29 | with:
30 | dotnet-version: '8.0.x'
31 | - name: Setup .NET 9.0 SDK
32 | uses: actions/setup-dotnet@v4
33 | with:
34 | dotnet-version: '9.0.x'
35 | - name: Restore
36 | run: dotnet restore
37 | - name: Build
38 | run: dotnet build --configuration Release --no-restore
39 | - name: Test
40 | run: dotnet test --configuration Release --no-build
--------------------------------------------------------------------------------
/.github/workflows/gh-pages.yml:
--------------------------------------------------------------------------------
1 | name: DocFx
2 | on: workflow_dispatch
3 | jobs:
4 | build:
5 | runs-on: windows-latest
6 | steps:
7 | - name: Checkout
8 | uses: actions/checkout@v4
9 | - name: Setup .NET Core 3.1 SDK
10 | uses: actions/setup-dotnet@v4
11 | with:
12 | dotnet-version: '3.1.x'
13 | - name: Setup .NET 5.0 SDK
14 | uses: actions/setup-dotnet@v4
15 | with:
16 | dotnet-version: '5.0.x'
17 | - name: Setup .NET 6.0 SDK
18 | uses: actions/setup-dotnet@v4
19 | with:
20 | dotnet-version: '6.0.x'
21 | - name: Setup .NET 8.0 SDK
22 | uses: actions/setup-dotnet@v4
23 | with:
24 | dotnet-version: '8.0.x'
25 | - name: Setup .NET 9.0 SDK
26 | uses: actions/setup-dotnet@v4
27 | with:
28 | dotnet-version: '9.0.x'
29 | - name: Restore
30 | run: dotnet restore
31 | - name: Build
32 | run: dotnet build --configuration Release --no-restore
33 | - name: Upload Artifacts
34 | uses: actions/upload-artifact@v4
35 | with:
36 | name: docfx
37 | path: docs/DocFx.Net.Http.WebPush/wwwroot
38 | retention-days: 1
39 | publish:
40 | needs: [build]
41 | runs-on: ubuntu-latest
42 | steps:
43 | - name: Checkout
44 | uses: actions/checkout@v4
45 | - name: Download Artifacts
46 | uses: actions/download-artifact@v4
47 | with:
48 | name: docfx
49 | path: docs/DocFx.Net.Http.WebPush/wwwroot
50 | - name: Publish
51 | uses: JamesIves/github-pages-deploy-action@4.1.5
52 | with:
53 | branch: gh-pages
54 | folder: docs/DocFx.Net.Http.WebPush/wwwroot
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | # .NET Core
46 | project.lock.json
47 | project.fragment.lock.json
48 | artifacts/
49 | **/Properties/launchSettings.json
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # Visual Studio code coverage results
117 | *.coverage
118 | *.coveragexml
119 |
120 | # NCrunch
121 | _NCrunch_*
122 | .*crunch*.local.xml
123 | nCrunchTemp_*
124 |
125 | # MightyMoose
126 | *.mm.*
127 | AutoTest.Net/
128 |
129 | # Web workbench (sass)
130 | .sass-cache/
131 |
132 | # Installshield output folder
133 | [Ee]xpress/
134 |
135 | # DocProject is a documentation generator add-in
136 | DocProject/buildhelp/
137 | DocProject/Help/*.HxT
138 | DocProject/Help/*.HxC
139 | DocProject/Help/*.hhc
140 | DocProject/Help/*.hhk
141 | DocProject/Help/*.hhp
142 | DocProject/Help/Html2
143 | DocProject/Help/html
144 |
145 | # DocFX generated stuff
146 | docs/DocFx.Net.Http.WebPush/log.txt
147 | docs/DocFx.Net.Http.WebPush/api/
148 | docs/DocFx.Net.Http.WebPush/wwwroot/
149 |
150 | # Click-Once directory
151 | publish/
152 |
153 | # Publish Web Output
154 | *.[Pp]ublish.xml
155 | *.azurePubxml
156 | # TODO: Comment the next line if you want to checkin your web deploy settings
157 | # but database connection strings (with potential passwords) will be unencrypted
158 | *.pubxml
159 | *.publishproj
160 |
161 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
162 | # checkin your Azure Web App publish settings, but sensitive information contained
163 | # in these scripts will be unencrypted
164 | PublishScripts/
165 |
166 | # NuGet Packages
167 | *.nupkg
168 | # The packages folder can be ignored because of Package Restore
169 | **/packages/*
170 | # except build/, which is used as an MSBuild target.
171 | !**/packages/build/
172 | # Uncomment if necessary however generally it will be regenerated when needed
173 | #!**/packages/repositories.config
174 | # NuGet v3's project.json files produces more ignorable files
175 | *.nuget.props
176 | *.nuget.targets
177 |
178 | # Microsoft Azure Build Output
179 | csx/
180 | *.build.csdef
181 |
182 | # Microsoft Azure Emulator
183 | ecf/
184 | rcf/
185 |
186 | # Windows Store app package directories and files
187 | AppPackages/
188 | BundleArtifacts/
189 | Package.StoreAssociation.xml
190 | _pkginfo.txt
191 |
192 | # Visual Studio cache files
193 | # files ending in .cache can be ignored
194 | *.[Cc]ache
195 | # but keep track of directories ending in .cache
196 | !*.[Cc]ache/
197 |
198 | # Others
199 | ClientBin/
200 | ~$*
201 | *~
202 | *.dbmdl
203 | *.dbproj.schemaview
204 | *.jfm
205 | *.pfx
206 | *.publishsettings
207 | orleans.codegen.cs
208 |
209 | # Since there are multiple workflows, uncomment next line to ignore bower_components
210 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
211 | #bower_components/
212 |
213 | # RIA/Silverlight projects
214 | Generated_Code/
215 |
216 | # Backup & report files from converting an old project file
217 | # to a newer Visual Studio version. Backup files are not needed,
218 | # because we have git ;-)
219 | _UpgradeReport_Files/
220 | Backup*/
221 | UpgradeLog*.XML
222 | UpgradeLog*.htm
223 |
224 | # SQL Server files
225 | *.mdf
226 | *.ldf
227 | *.ndf
228 |
229 | # Business Intelligence projects
230 | *.rdl.data
231 | *.bim.layout
232 | *.bim_*.settings
233 |
234 | # Microsoft Fakes
235 | FakesAssemblies/
236 |
237 | # GhostDoc plugin setting file
238 | *.GhostDoc.xml
239 |
240 | # Node.js Tools for Visual Studio
241 | .ntvs_analysis.dat
242 | node_modules/
243 |
244 | # Typescript v1 declaration files
245 | typings/
246 |
247 | # Visual Studio 6 build log
248 | *.plg
249 |
250 | # Visual Studio 6 workspace options file
251 | *.opt
252 |
253 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
254 | *.vbw
255 |
256 | # Visual Studio LightSwitch build output
257 | **/*.HTMLClient/GeneratedArtifacts
258 | **/*.DesktopClient/GeneratedArtifacts
259 | **/*.DesktopClient/ModelManifest.xml
260 | **/*.Server/GeneratedArtifacts
261 | **/*.Server/ModelManifest.xml
262 | _Pvt_Extensions
263 |
264 | # Paket dependency manager
265 | .paket/paket.exe
266 | paket-files/
267 |
268 | # FAKE - F# Make
269 | .fake/
270 |
271 | # JetBrains Rider
272 | .idea/
273 | *.sln.iml
274 |
275 | # CodeRush
276 | .cr/
277 |
278 | # Python Tools for Visual Studio (PTVS)
279 | __pycache__/
280 | *.pyc
281 |
282 | # Cake - Uncomment if you are using it
283 | # tools/**
284 | # !tools/packages.config
285 |
286 | # Telerik's JustMock configuration file
287 | *.jmconfig
288 |
289 | # BizTalk build output
290 | *.btp.cs
291 | *.btm.cs
292 | *.odx.cs
293 | *.xsd.cs
294 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Lib.Net.Http.WebPush 3.3.1
2 | ### Bug Fixes
3 | - Fix for `RequestPushMessageDeliveryAsync` unable to write content to request stream for empty content.
4 |
5 | ## Lib.Net.Http.WebPush 3.3.0
6 | ### Additions and Changes
7 | - Added support for Mozilla Autopush Server return codes
8 |
9 | ## Lib.Net.Http.WebPush 3.2.1
10 | ### Bug Fixes
11 | - Fix for client keys casing and validation.
12 |
13 | ## Lib.Net.Http.WebPush 3.2.0
14 | ### Additions and Changes
15 | - Dropped support for .NET Core 2.1
16 | - Added support for .NET 6.0
17 |
18 | ## Lib.Net.Http.WebPush 3.1.0
19 | ### Additions and Changes
20 | - Dropped support for .NET Standard 1.6
21 | - Removed dependency on Bouncy Castle for .NET 5
22 | - `VapidAuthentication` now implements `IDisposable`
23 |
24 | ## Lib.Net.Http.WebPush 2.3.0
25 | ### Bug Fixes
26 | - Fix for incorrect reuse of internally created `HttpContent`
27 | - Fix for incorrect reuse of internally created `HttpRequestMessage`
28 | ### Additions and Changes
29 | - Added context information to `PushServiceClientException` (thank you @techfg)
30 | - Added setting for maximum limit of automatic retries in case of 429 Too Many Requests
31 | - Exposed `AutoRetryAfter`, `DefaultTimeToLive`, and `MaxRetriesAfter` in Azure Functions binding
32 |
33 | ## Lib.Net.Http.WebPush 2.2.0
34 | ### Additions and Changes
35 | - Added `HttpContent` based constructor for `PushMessage`
36 |
37 | ## Lib.Net.Http.WebPush 2.1.0
38 | ### Additions and Changes
39 | - Automatic support for retries in case of 429 Too Many Requests
40 |
41 | ## Lib.Net.Http.WebPush 2.0.0
42 | ### Additions and Changes
43 | - Strong named the assembly
44 | - Changed default authentication scheme to VAPID
45 |
46 | ## Lib.Net.Http.WebPush 1.5.0
47 | ### Bug Fixes
48 | - Fix for *Length Required* issue in case of push message delivery request to MS Edge
49 |
50 | ## Lib.Net.Http.WebPush 1.4.0
51 | ### Additions and Changes
52 | - Added support for .NET Standard 2.0
53 |
54 | ## Lib.Net.Http.WebPush 1.3.0
55 | ### Bug Fixes
56 | - Fix for push messages urgency
57 | ### Additions and Changes
58 | - Added constructor which accepts instance of `HttpClient`
59 | - Minor performance improvements
60 |
61 | ## Lib.Net.Http.WebPush 1.2.0
62 | ### Additions and Changes
63 | - Added support for push messages topic
64 | - Added support for push messages urgency
65 |
66 | ## Lib.Net.Http.WebPush 1.1.0
67 | ### Additions and Changes
68 | - Added VAPID tokens caching capability
69 | - Added support for both VAPID schemes
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 - 2025 Tomasz Pęczek
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/Lib.Net.Http.WebPush.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.2.32505.173
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lib.Net.Http.WebPush", "src\Lib.Net.Http.WebPush\Lib.Net.Http.WebPush.csproj", "{79FDCAAA-6129-4FBB-B94E-7F7BE9A07C7C}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocFx.Net.Http.WebPush", "docs\DocFx.Net.Http.WebPush\DocFx.Net.Http.WebPush.csproj", "{91C4FA65-7104-494C-9ED8-4B886BC2F7CD}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmark.Net.Http.WebPush", "benchmarks\Benchmark.Net.Http.WebPush\Benchmark.Net.Http.WebPush.csproj", "{0100508D-6C06-443B-9893-91F2F15DC920}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lib.Azure.WebJobs.Extensions.WebPush", "src\Lib.Azure.WebJobs.Extensions.WebPush\Lib.Azure.WebJobs.Extensions.WebPush.csproj", "{001A4013-EE58-428A-BD6C-49AC3A0CA6F9}"
13 | EndProject
14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0441724F-48F7-4863-BBA6-586E10182605}"
15 | EndProject
16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{C24A444A-8A7A-4BE1-84C4-3E9755D5F9BE}"
17 | EndProject
18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{10B2C3B8-F5B5-49D4-ADA2-48FE376B758A}"
19 | EndProject
20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lib.AspNetCore.WebPush", "src\Lib.AspNetCore.WebPush\Lib.AspNetCore.WebPush.csproj", "{D9CF3828-E0F8-4A92-97D3-4B79E9C38F58}"
21 | EndProject
22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{AFD9AC51-E923-49DF-B000-721816ABDE37}"
23 | EndProject
24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test.Lib.Net.Http.WebPush", "test\Test.Lib.Net.Http.WebPush\Test.Lib.Net.Http.WebPush.csproj", "{E0746BE4-61F3-4366-8C7E-99C2350A0DD3}"
25 | EndProject
26 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7992C65C-E6C3-4B34-AB9B-EF24915BB907}"
27 | ProjectSection(SolutionItems) = preProject
28 | CHANGELOG.md = CHANGELOG.md
29 | README.md = README.md
30 | EndProjectSection
31 | EndProject
32 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lib.Azure.Functions.Worker.Extensions.WebPush", "src\Lib.Azure.Functions.Worker.Extensions.WebPush\Lib.Azure.Functions.Worker.Extensions.WebPush.csproj", "{FF54B26A-8E32-4D2F-B91E-C8BAAFE9148D}"
33 | EndProject
34 | Global
35 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
36 | Debug|Any CPU = Debug|Any CPU
37 | Release|Any CPU = Release|Any CPU
38 | EndGlobalSection
39 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
40 | {79FDCAAA-6129-4FBB-B94E-7F7BE9A07C7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41 | {79FDCAAA-6129-4FBB-B94E-7F7BE9A07C7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
42 | {79FDCAAA-6129-4FBB-B94E-7F7BE9A07C7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
43 | {79FDCAAA-6129-4FBB-B94E-7F7BE9A07C7C}.Release|Any CPU.Build.0 = Release|Any CPU
44 | {91C4FA65-7104-494C-9ED8-4B886BC2F7CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
45 | {91C4FA65-7104-494C-9ED8-4B886BC2F7CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
46 | {91C4FA65-7104-494C-9ED8-4B886BC2F7CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
47 | {91C4FA65-7104-494C-9ED8-4B886BC2F7CD}.Release|Any CPU.Build.0 = Release|Any CPU
48 | {0100508D-6C06-443B-9893-91F2F15DC920}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
49 | {0100508D-6C06-443B-9893-91F2F15DC920}.Debug|Any CPU.Build.0 = Debug|Any CPU
50 | {0100508D-6C06-443B-9893-91F2F15DC920}.Release|Any CPU.ActiveCfg = Release|Any CPU
51 | {0100508D-6C06-443B-9893-91F2F15DC920}.Release|Any CPU.Build.0 = Release|Any CPU
52 | {001A4013-EE58-428A-BD6C-49AC3A0CA6F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
53 | {001A4013-EE58-428A-BD6C-49AC3A0CA6F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
54 | {001A4013-EE58-428A-BD6C-49AC3A0CA6F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
55 | {001A4013-EE58-428A-BD6C-49AC3A0CA6F9}.Release|Any CPU.Build.0 = Release|Any CPU
56 | {D9CF3828-E0F8-4A92-97D3-4B79E9C38F58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
57 | {D9CF3828-E0F8-4A92-97D3-4B79E9C38F58}.Debug|Any CPU.Build.0 = Debug|Any CPU
58 | {D9CF3828-E0F8-4A92-97D3-4B79E9C38F58}.Release|Any CPU.ActiveCfg = Release|Any CPU
59 | {D9CF3828-E0F8-4A92-97D3-4B79E9C38F58}.Release|Any CPU.Build.0 = Release|Any CPU
60 | {E0746BE4-61F3-4366-8C7E-99C2350A0DD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
61 | {E0746BE4-61F3-4366-8C7E-99C2350A0DD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
62 | {E0746BE4-61F3-4366-8C7E-99C2350A0DD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
63 | {E0746BE4-61F3-4366-8C7E-99C2350A0DD3}.Release|Any CPU.Build.0 = Release|Any CPU
64 | {FF54B26A-8E32-4D2F-B91E-C8BAAFE9148D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
65 | {FF54B26A-8E32-4D2F-B91E-C8BAAFE9148D}.Debug|Any CPU.Build.0 = Debug|Any CPU
66 | {FF54B26A-8E32-4D2F-B91E-C8BAAFE9148D}.Release|Any CPU.ActiveCfg = Release|Any CPU
67 | {FF54B26A-8E32-4D2F-B91E-C8BAAFE9148D}.Release|Any CPU.Build.0 = Release|Any CPU
68 | EndGlobalSection
69 | GlobalSection(SolutionProperties) = preSolution
70 | HideSolutionNode = FALSE
71 | EndGlobalSection
72 | GlobalSection(NestedProjects) = preSolution
73 | {79FDCAAA-6129-4FBB-B94E-7F7BE9A07C7C} = {0441724F-48F7-4863-BBA6-586E10182605}
74 | {91C4FA65-7104-494C-9ED8-4B886BC2F7CD} = {C24A444A-8A7A-4BE1-84C4-3E9755D5F9BE}
75 | {0100508D-6C06-443B-9893-91F2F15DC920} = {10B2C3B8-F5B5-49D4-ADA2-48FE376B758A}
76 | {001A4013-EE58-428A-BD6C-49AC3A0CA6F9} = {0441724F-48F7-4863-BBA6-586E10182605}
77 | {D9CF3828-E0F8-4A92-97D3-4B79E9C38F58} = {0441724F-48F7-4863-BBA6-586E10182605}
78 | {E0746BE4-61F3-4366-8C7E-99C2350A0DD3} = {AFD9AC51-E923-49DF-B000-721816ABDE37}
79 | {FF54B26A-8E32-4D2F-B91E-C8BAAFE9148D} = {0441724F-48F7-4863-BBA6-586E10182605}
80 | EndGlobalSection
81 | GlobalSection(ExtensibilityGlobals) = postSolution
82 | SolutionGuid = {88535C7E-5564-4AC1-A41B-18DD05637943}
83 | EndGlobalSection
84 | EndGlobal
85 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lib.Net.Http.WebPush
2 |
3 | [](https://www.nuget.org/packages/Lib.Net.Http.WebPush)
4 | [](https://www.nuget.org/packages/Lib.Net.Http.WebPush)
5 |
6 | [](https://www.nuget.org/packages/Lib.AspNetCore.WebPush)
7 | [](https://www.nuget.org/packages/Lib.AspNetCore.WebPush)
8 |
9 | [](https://www.nuget.org/packages/Lib.Azure.WebJobs.Extensions.WebPush)
10 | [](https://www.nuget.org/packages/Lib.Azure.WebJobs.Extensions.WebPush)
11 |
12 | [](https://www.nuget.org/packages/Lib.Azure.Functions.Worker.Extensions.WebPush)
13 | [](https://www.nuget.org/packages/Lib.Azure.Functions.Worker.Extensions.WebPush)
14 |
15 | Lib.Net.Http.WebPush is a library which provides a [Web Push Protocol](https://tools.ietf.org/html/rfc8030) based client for Push Service. It provides support for [Voluntary Application Server Identification (VAPID) for Web Push](https://tools.ietf.org/html/rfc8292) and [Message Encryption for Web Push](https://tools.ietf.org/html/rfc8291).
16 |
17 | Lib.AspNetCore.WebPush is a library which provides ASP.NET Core extensions for Web Push Protocol based client for Push Service.
18 |
19 | Lib.Azure.WebJobs.Extensions.WebPush is a library which provides [Azure Functions](https://functions.azure.com/) in-process model and [Azure WebJobs](https://docs.microsoft.com/en-us/azure/app-service/web-sites-create-web-jobs) binding extensions for Web Push Protocol based client for Push Service.
20 |
21 | Lib.Azure.Functions.Worker.Extensions.WebPush is a library which provides [Azure Functions](https://functions.azure.com/) isolated worker model extensions for Web Push Protocol based client for Push Service.
22 |
23 | ## Installation
24 |
25 | You can install [Lib.Net.Http.WebPush](https://www.nuget.org/packages/Lib.Net.Http.WebPush), [Lib.AspNetCore.WebPush](https://www.nuget.org/packages/Lib.AspNetCore.WebPush), [Lib.Azure.WebJobs.Extensions.WebPush](https://www.nuget.org/packages/Lib.Azure.WebJobs.Extensions.WebPush), and [Lib.Azure.Functions.Worker.Extensions.WebPush](https://www.nuget.org/packages/Lib.Azure.Functions.Worker.Extensions.WebPush) from NuGet.
26 |
27 | ```
28 | PM> Install-Package Lib.Net.Http.WebPush
29 | ```
30 |
31 | ```
32 | PM> Install-Package Lib.AspNetCore.WebPush
33 | ```
34 |
35 | ```
36 | PM> Install-Package Lib.Azure.WebJobs.Extensions.WebPush
37 | ```
38 |
39 | ```
40 | PM> Install-Package Lib.Azure.Functions.Worker.Extensions.WebPush
41 | ```
42 |
43 | ## Documentation
44 |
45 | The documentation is available [here](https://tpeczek.github.io/Lib.Net.Http.WebPush/).
46 |
47 | ## Demos
48 |
49 | There are several demo projects available:
50 | - [Web Push Notifications in ASP.NET Core Web Application](https://github.com/tpeczek/Demo.AspNetCore.PushNotifications)
51 | - [Web Push Notifications in ASP.NET Core-powered Angular Application](https://github.com/tpeczek/Demo.AspNetCore.Angular.PushNotifications)
52 | - [Web Push Notifications in Azure Functions](https://github.com/tpeczek/Demo.Azure.Funtions.PushNotifications)
53 |
54 | ## Donating
55 |
56 | My blog and open source projects are result of my passion for software development, but they require a fair amount of my personal time. If you got value from any of the content I create, then I would appreciate your support by [sponsoring me](https://github.com/sponsors/tpeczek) (either monthly or one-time).
57 |
58 | ## Copyright and License
59 |
60 | Copyright © 2018 - 2025 Tomasz Pęczek
61 |
62 | Licensed under the [MIT License](https://github.com/tpeczek/Lib.Net.Http.WebPush/blob/master/LICENSE.md)
--------------------------------------------------------------------------------
/benchmarks/Benchmark.Net.Http.WebPush/Benchmark.Net.Http.WebPush.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Exe
4 | net6.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/benchmarks/Benchmark.Net.Http.WebPush/Program.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Reports;
2 | using BenchmarkDotNet.Running;
3 | using System;
4 |
5 | namespace Benchmark.Net.Http.WebPush
6 | {
7 | class Program
8 | {
9 | static void Main(string[] args)
10 | {
11 | Summary webPushBenchmarksSummary = BenchmarkRunner.Run();
12 |
13 | Console.ReadKey();
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/benchmarks/Benchmark.Net.Http.WebPush/WebPushBenchmarks.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Net.Http;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using System.Collections.Generic;
7 | using BenchmarkDotNet.Attributes;
8 | using Lib.Net.Http.WebPush;
9 | using Lib.Net.Http.WebPush.Authentication;
10 | using static Lib.Net.Http.WebPush.Authentication.VapidAuthentication;
11 |
12 | namespace Benchmark.Net.Http.WebPush
13 | {
14 | [MemoryDiagnoser]
15 | public class WebPushBenchmarks
16 | {
17 | #region Classes
18 | private class RequestPushMessageDeliveryVapidTokenCache : IVapidTokenCache
19 | {
20 | public string Get(string audience)
21 | {
22 | return "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTUxODMzMzkyNCwic3ViIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NjU1MDYvIn0.RP8t19y_3c6ncoE7iHOukEEKxIb8nBHdOaeY8xzLoMw62-GlWR5C1Rp8iG2rex9_pk_1LR4MJSAkMpDbhnZo5w";
23 | }
24 |
25 | public void Put(string audience, DateTimeOffset expiration, string token)
26 | { }
27 | }
28 |
29 | private class RequestPushMessageDeliveryHttpMessageHandler : HttpMessageHandler
30 | {
31 | Task _createdResponseMessageTask = Task.FromResult(new HttpResponseMessage(HttpStatusCode.Created));
32 |
33 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
34 | {
35 | return _createdResponseMessageTask;
36 | }
37 | }
38 | #endregion
39 |
40 | #region Fields
41 | private readonly PushMessage _message = new PushMessage("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse sit amet dolor tristique, tempus mi ultrices, vulputate erat. Nulla viverra mauris et ante pharetra accumsan. Praesent venenatis nibh vel aliquam pharetra. Suspendisse massa justo, mollis vitae venenatis eu, mattis sit amet ipsum. Etiam dolor lacus, vulputate id congue eu, mattis vel orci. Curabitur dignissim posuere vehicula. Quisque tristique tellus ligula, sit amet tincidunt sapien dignissim ut. Quisque pulvinar justo non turpis vehicula, hendrerit semper mi gravida. In vitae massa et erat commodo dignissim at id massa. Etiam non fringilla dolor, eu sagittis elit. Vestibulum finibus molestie viverra. Duis urna libero, pulvinar et mollis sed, volutpat sed orci. Sed dapibus vitae urna a volutpat. Phasellus ultricies eget quam commodo sagittis. Nunc sollicitudin ullamcorper faucibus. Vestibulum tempus molestie justo, at tristique urna finibus vel. Nam vitae eros gravida, tincidunt lorem quis, lacinia dui. Aliquam pretium metus nec risus scelerisque, eget auctor quam maximus. Vestibulum tempor metus egestas, maximus felis id, cursus diam. Sed non libero quis nibh scelerisque pellentesque vel nec nunc. Aliquam luctus ornare justo, at fermentum orci scelerisque at. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Etiam tempus finibus sem ac egestas. Fusce vitae justo ligula. Integer vel felis maximus odio commodo auctor eu quis nisl. Interdum et malesuada fames ac ante ipsum primis in faucibus. Suspendisse finibus mauris mattis luctus luctus. Integer imperdiet turpis vitae elit interdum sagittis. Aenean semper at sapien sit amet porttitor. Proin vel lacus vestibulum, consectetur urna sed, ultricies eros. Integer feugiat enim quis posuere ultricies. Quisque ac mattis diam. Nulla non blandit neque. Vivamus non elementum leo, eget vestibulum odio. Curabitur dignissim justo urna, quis imperdiet metus porttitor a. Nunc porta justo erat, vel malesuada mauris consectetur sed. Etiam fermentum dapibus mi, a ultrices ex euismod ut. Vivamus fringilla, sem ac pulvinar auctor, arcu mauris viverra erat, nec tempor orci nunc nec ipsum. Donec hendrerit eros aliquam finibus efficitur. Aliquam id sem eu urna volutpat consequat. Integer varius tellus eget sem cursus laoreet. Suspendisse potenti. Duis a dolor eros. Proin aliquet nisl dui, at faucibus odio porttitor a. Ut at fermentum purus, vel tincidunt massa. Aliquam ornare ornare augue ut ullamcorper. In cursus auctor purus, sodales suscipit enim sagittis vel. Etiam nec semper magna. Vivamus a mi congue, efficitur nisl vitae, sagittis urna. Vivamus ut ex a enim commodo feugiat. Nam dictum egestas neque eget tristique. Nam interdum mi non ornare rhoncus. Nam mi arcu, placerat sollicitudin ligula eu, efficitur hendrerit elit. Aliquam suscipit lacinia ante ac suscipit. Aliquam malesuada dolor at lorem mattis, ut feugiat mauris semper. Nulla tortor leo, porttitor eu ipsum vel, lobortis bibendum tortor. Quisque semper quam at cursus laoreet. Fusce in facilisis orci. Vivamus dapibus ac arcu ut condimentum. Morbi mollis a turpis nec sollicitudin. Aliquam fringilla massa elit, a mollis velit rhoncus eget. Aliquam erat volutpat. Vestibulum ante mauris, aliquet non purus ac, ornare pretium neque. Maecenas nec tortor justo. Curabitur ullamcorper placerat mauris vel scelerisque. Quisque maximus nunc sit amet aliquam pulvinar. Maecenas ullamcorper dictum efficitur. Proin auctor, sapien nec efficitur semper, dolor purus faucibus ipsum, fringilla consequat ipsum magna quis dui. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam erat volutpat. Suspendisse potenti. Maecenas orci odio, interdum ut lacus ac, sagittis molestie turpis. Donec pretium diam quis ex aliquam, quis rutrum ipsum egestas. Maecenas eu leo finibus, tincidunt leo sed, accumsan tellus. Sed aliquam placerat lectus, at tempor massa volutpat id. Integer a viverra ipsum. Etiam eu gravida risus. Maecenas turpis, porta vitae dignissim a, facilisis ac tortor. Nulla nec sed.");
42 | private readonly PushSubscription _subscription = new PushSubscription
43 | {
44 | Endpoint = "https://fcm.googleapis.com/fcm/send/fvi1URmMPCw:APA91bGqK_mH7LfRzY1wKeTj_kpealAK_HhRKpTNPSQ7dW8TQjR4qK4a7BcZcMRIHU9A6cH6iimqP7zyirmxjKd4vOKwos_itEI78bVF0LqCn5Xv99QkMxW_YZR7d2j993LapxJvzZ_V",
45 | Keys = new Dictionary
46 | {
47 | { "p256dh", "BP7M3CU6iBsNCJZAYk2akn2j236QjtxXZ6hMZP9H6XKTHFmjG1Al-fVW49Dz8Zlr_Qtqz30fv3yNrRelpj8Gvtg=" },
48 | { "auth", "YE41bJSLtSq-hraJ17ryyA==" }
49 | }
50 | };
51 |
52 | private VapidAuthentication _vapidAuthentication = new VapidAuthentication("BK5sn4jfa0Jqo9MhV01oyzK2FaEHm0KqkSCuUkKr53-9cr-vBE1a9TiiBaWy7hy0eOUF1jhZnwcd3vof4wnwSw0", "AJ2ho7or-6D4StPktpTO3l1ErjHGyxb0jzt9Y8lj67g")
53 | {
54 | Subject = "https://localhost:65506/"
55 | };
56 |
57 | private PushServiceClient _pushClient = new PushServiceClient(new HttpClient(new RequestPushMessageDeliveryHttpMessageHandler()))
58 | {
59 | DefaultAuthentication = new VapidAuthentication("BK5sn4jfa0Jqo9MhV01oyzK2FaEHm0KqkSCuUkKr53-9cr-vBE1a9TiiBaWy7hy0eOUF1jhZnwcd3vof4wnwSw0", "AJ2ho7or-6D4StPktpTO3l1ErjHGyxb0jzt9Y8lj67g")
60 | {
61 | Subject = "https://localhost:65506/",
62 | TokenCache = new RequestPushMessageDeliveryVapidTokenCache()
63 | }
64 | };
65 | #endregion
66 |
67 | #region Benchmarks
68 | [Benchmark]
69 | public void VapidAuthentication_GetWebPushSchemeHeadersValues()
70 | {
71 | WebPushSchemeHeadersValues webPushSchemeHeadersValues = _vapidAuthentication.GetWebPushSchemeHeadersValues("https://fcm.googleapis.com");
72 | }
73 |
74 | [Benchmark]
75 | public void VapidAuthentication_GetVapidSchemeAuthenticationHeaderValueParameter()
76 | {
77 | string vapidSchemeAuthenticationHeaderValueParameter = _vapidAuthentication.GetVapidSchemeAuthenticationHeaderValueParameter("https://fcm.googleapis.com");
78 | }
79 |
80 | [Benchmark]
81 | public Task PushServiceClient_RequestPushMessageDeliveryAsync()
82 | {
83 | return _pushClient.RequestPushMessageDeliveryAsync(_subscription, _message);
84 | }
85 | #endregion
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/docs/DocFx.Net.Http.WebPush/DocFx.Net.Http.WebPush.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | all
14 | runtime; build; native; contentfiles; analyzers
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/docs/DocFx.Net.Http.WebPush/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore;
2 | using Microsoft.AspNetCore.Hosting;
3 |
4 | namespace DocFx.Net.Http.WebPush
5 | {
6 | public class Program
7 | {
8 | public static void Main(string[] args) => WebHost.CreateDefaultBuilder(args).UseStartup().Build().Run();
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/docs/DocFx.Net.Http.WebPush/Startup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.Extensions.Hosting;
3 | using Microsoft.Extensions.DependencyInjection;
4 |
5 | namespace DocFx.Net.Http.WebPush
6 | {
7 | public class Startup
8 | {
9 | public void ConfigureServices(IServiceCollection services)
10 | { }
11 |
12 | public void Configure(IApplicationBuilder app, IHostEnvironment env)
13 | {
14 | app.UseDefaultFiles()
15 | .UseStaticFiles();
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/docs/DocFx.Net.Http.WebPush/articles/aspnetcore-integration.md:
--------------------------------------------------------------------------------
1 | # PushServiceClient extensions for ASP.NET Core
2 |
3 | The [`PushServiceClient`](../api/Lib.Net.Http.WebPush.PushServiceClient.html) extensions for ASP.NET Core provide an easy way to configure and create `PushServiceClient` instances in an app.
4 |
5 | ## Basic usage
6 |
7 | The `PushServiceClient` factory (under the hood it uses `IHttpClientFactory`) can be registered by calling the [`AddPushServiceClient`](../api/Microsoft.Extensions.DependencyInjection.PushServiceClientServiceCollectionExtensions.html#Microsoft_Extensions_DependencyInjection_PushServiceClientServiceCollectionExtensions_AddPushServiceClient_Microsoft_Extensions_DependencyInjection_IServiceCollection_) extension method on the `IServiceCollection`, inside the `Startup.ConfigureServices` method. During registration there is an option to provide default authentication and configuration for a `PushServiceClient`.
8 |
9 | ```cs
10 | services.AddPushServiceClient(options =>
11 | {
12 | ...
13 |
14 | options.PublicKey = "";
15 | options.PrivateKey = "";
16 | });
17 | ```
18 |
19 | Once registered, code can accept a `PushServiceClient` anywhere services can be injected with dependency injection (DI).
20 |
21 | ```cs
22 | internal class PushNotificationsDequeuer : IHostedService
23 | {
24 | private readonly PushServiceClient _pushClient;
25 |
26 | public PushNotificationsDequeuer(PushServiceClient pushClient)
27 | {
28 | _pushClient = pushClient;
29 | }
30 | }
31 | ```
32 |
33 | ## VAPID Tokens Caching
34 |
35 | There is also an option to enable *VAPID* tokens caching by calling the [`AddMemoryVapidTokenCache`](../api/Microsoft.Extensions.DependencyInjection.PushServiceClientServiceCollectionExtensions.html#Microsoft_Extensions_DependencyInjection_PushServiceClientServiceCollectionExtensions_AddMemoryVapidTokenCache_Microsoft_Extensions_DependencyInjection_IServiceCollection_) or [`AddDistributedVapidTokenCache`](../api/Microsoft.Extensions.DependencyInjection.PushServiceClientServiceCollectionExtensions.html#Microsoft_Extensions_DependencyInjection_PushServiceClientServiceCollectionExtensions_AddDistributedVapidTokenCache_Microsoft_Extensions_DependencyInjection_IServiceCollection_) mehod.
36 |
37 | ```cs
38 | services.AddMemoryCache();
39 | services.AddMemoryVapidTokenCache();
40 | services.AddPushServiceClient(options =>
41 | {
42 | ...
43 | });
44 | ```
45 |
46 | Once a *VAPID* tokens cache is registered the factory will start using it automatically.
--------------------------------------------------------------------------------
/docs/DocFx.Net.Http.WebPush/articles/azure-functions-integration.md:
--------------------------------------------------------------------------------
1 | # PushServiceClient bindings for Azure Functions
2 |
3 | The [`PushServiceClient`](../api/Lib.Net.Http.WebPush.PushServiceClient.html) extensions for Azure Functions supports input binding.
4 |
5 | ## Packages
6 |
7 | The [`PushServiceClient`](../api/Lib.Net.Http.WebPush.PushServiceClient.html) extensions for Azure Functions are provided in the [Lib.Azure.WebJobs.Extensions.WebPush](https://www.nuget.org/packages/Lib.Azure.WebJobs.Extensions.WebPush) (in-process model) and [Lib.Azure.Functions.Worker.Extensions.WebPush](https://www.nuget.org/packages/Lib.Azure.Functions.Worker.Extensions.WebPush) (isolated worker model) NuGet packages.
8 |
9 | ```
10 | PM> Install-Package Lib.Azure.WebJobs.Extensions.WebPush
11 | ```
12 |
13 | ```
14 | PM> Install-Package Lib.Azure.Functions.Worker.Extensions.WebPush
15 | ```
16 |
17 | ## Input
18 |
19 | The [`PushServiceClient`](../api/Lib.Net.Http.WebPush.PushServiceClient.html) input binding uses `HttpClientFactory` to retrieve [`PushServiceClient`](../api/Lib.Net.Http.WebPush.PushServiceClient.html) instance and passes it to the input parameter of the function.
20 |
21 | ### Input - language-specific examples
22 |
23 | #### Input - C# examples
24 |
25 | ##### Isolated Worker Model
26 | In [the isolated worker model functions](https://learn.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-process-guide?tabs=windows), use the [`PushServiceInput`](../api/Lib.Azure.Functions.Worker.Extensions.WebPush.PushServiceInputAttribute.html) attribute.
27 |
28 | The attribute's constructor takes the application server public key and application server private key. For information about those settings and other properties that you can configure, see [Input - configuration](#input---configuration).
29 |
30 | ###### Azure Cosmos DB trigger, subscriptions from Azure Cosmos DB
31 | The following example shows a [C# function](https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-class-library) that broadcasts a notification to all known subscriptions. The function is triggered by a change in Azure Cosmos DB collection and retrieves subscriptions also from Azure Cosmos DB.
32 |
33 | ```cs
34 | ...
35 |
36 | namespace Demo.Azure.Functions.Worker.PushNotifications
37 | {
38 | public class SendNotificationFunction
39 | {
40 | private readonly ILogger _logger;
41 |
42 | public SendNotificationFunction(ILoggerFactory loggerFactory)
43 | {
44 | _logger = loggerFactory.CreateLogger();
45 | }
46 |
47 | [Function("SendNotificationFunction")]
48 | public async Task Run([CosmosDBTrigger(
49 | databaseName: "PushNotifications",
50 | containerName: "Notifications",
51 | Connection = "CosmosDBConnection",
52 | LeaseContainerName = "NotificationsLeaseCollection",
53 | CreateLeaseContainerIfNotExists = true)] IReadOnlyList notifications,
54 | [CosmosDBInput(
55 | databaseName: "PushNotifications",
56 | containerName: "Subscriptions",
57 | Connection = "CosmosDBConnection")] CosmosClient cosmosClient,
58 | [PushServiceInput(
59 | PublicKeySetting = "ApplicationServerPublicKey",
60 | PrivateKeySetting = "ApplicationServerPrivateKey",
61 | SubjectSetting = "ApplicationServerSubject")] PushServiceClient pushServiceClient)
62 | {
63 | if (notifications != null)
64 | {
65 | Container subscriptionsContainer = cosmosClient.GetDatabase("PushNotifications").GetContainer("Subscriptions");
66 | using (FeedIterator subscriptionsIterator = subscriptionsContainer.GetItemQueryIterator())
67 | {
68 | while (subscriptionsIterator.HasMoreResults)
69 | {
70 | foreach (PushSubscription subscription in await subscriptionsIterator.ReadNextAsync())
71 | {
72 | foreach (Notification notification in notifications)
73 | {
74 | // Fire-and-forget
75 | pushServiceClient.RequestPushMessageDeliveryAsync(subscription, new PushMessage(notification.Content)
76 | {
77 | Topic = notification.Topic,
78 | TimeToLive = notification.TimeToLive,
79 | Urgency = notification.Urgency
80 | });
81 | }
82 | }
83 | }
84 | }
85 | }
86 | }
87 | }
88 |
89 | public class Notification
90 | {
91 | public string? Topic { get; set; }
92 |
93 | public string Content { get; set; } = String.Empty;
94 |
95 | public int? TimeToLive { get; set; }
96 |
97 | public PushMessageUrgency Urgency { get; set; }
98 | }
99 | }
100 | ```
101 |
102 | ##### In-process Model
103 | In [C# class libraries](https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-class-library), use the [`PushService`](../api/Lib.Azure.WebJobs.Extensions.WebPush.Bindings.PushServiceAttribute.html) attribute.
104 |
105 | The attribute's constructor takes the application server public key and application server private key. For information about those settings and other properties that you can configure, see [Input - configuration](#input---configuration).
106 |
107 | ###### Azure Cosmos DB trigger, subscriptions from Azure Cosmos DB
108 | The following example shows a [C# function](https://docs.microsoft.com/en-us/azure/azure-functions/functions-dotnet-class-library) that broadcasts a notification to all known subscriptions. The function is triggered by a change in Azure Cosmos DB collection and retrieves subscriptions also from Azure Cosmos DB.
109 |
110 | ```cs
111 | ...
112 |
113 | namespace Demo.Azure.Funtions.PushNotifications
114 | {
115 | public class Notification
116 | {
117 | public string Topic { get; set; }
118 |
119 | public string Content { get; set; }
120 |
121 | public int? TimeToLive { get; set; }
122 |
123 | public PushMessageUrgency Urgency { get; set; }
124 | }
125 |
126 | public static class SendNotificationFunction
127 | {
128 | [FunctionName("SendNotification")]
129 | public static async Task Run([CosmosDBTrigger(
130 | databaseName: "PushNotifications",
131 | containerName: "Notifications",
132 | Connection = "CosmosDBConnection",
133 | LeaseContainerName = "NotificationsLeaseCollection",
134 | CreateLeaseContainerIfNotExists = true)]IReadOnlyList notifications,
135 | [CosmosDB(
136 | databaseName: "PushNotifications",
137 | containerName: "Subscriptions",
138 | Connection = "CosmosDBConnection")]CosmosClient cosmosClient,
139 | [PushService(
140 | PublicKeySetting = "ApplicationServerPublicKey",
141 | PrivateKeySetting = "ApplicationServerPrivateKey",
142 | SubjectSetting = "ApplicationServerSubject")]PushServiceClient pushServiceClient)
143 | {
144 | if (notifications != null)
145 | {
146 | Container subscriptionsContainer = cosmosClient.GetDatabase("PushNotifications").GetContainer("Subscriptions");
147 | using (FeedIterator subscriptionsIterator = subscriptionsContainer.GetItemQueryIterator())
148 | {
149 | while (subscriptionsIterator.HasMoreResults)
150 | {
151 | foreach (PushSubscription subscription in await subscriptionsIterator.ReadNextAsync())
152 | {
153 | foreach (Notification notification in notifications)
154 | {
155 | // Fire-and-forget
156 | pushServiceClient.RequestPushMessageDeliveryAsync(subscription, new PushMessage(notification.Content)
157 | {
158 | Topic = notification.Topic,
159 | TimeToLive = notification.TimeToLive,
160 | Urgency = notification.Urgency
161 | });
162 | }
163 | }
164 | }
165 | }
166 | }
167 | }
168 | }
169 | }
170 | ```
171 |
172 | ### Input - configuration
173 |
174 | The following table explains the binding configuration properties that you set in the *function.json* file and the [`PushService`](../api/Lib.Azure.WebJobs.Extensions.WebPush.Bindings.PushServiceAttribute.html) attribute.
175 |
176 | |function.json property | Attribute property |Description|
177 | |---------|---------|----------------------|
178 | |**type** || Must be set to `pushService`. |
179 | |**direction** || Must be set to `in`. |
180 | |**publicKeySetting**|**PublicKeySetting** | The name of an app setting that contains the application server public key. |
181 | |**privateKeySetting**|**PrivateKeySetting** | The name of an app setting that contains the application server private key. |
182 | |**subjectSetting**|**SubjectSetting** | The name of an app setting that contains the contact information for the application server. |
183 |
--------------------------------------------------------------------------------
/docs/DocFx.Net.Http.WebPush/articles/performance-considerations.md:
--------------------------------------------------------------------------------
1 | # Performance Considerations
2 |
3 | A typical application can be processing a very high number of *Push Messages*. Because of that it's important to consider performance best practices.
4 |
5 | ## Proper Instantiation
6 |
7 | The [`PushServiceClient`](../api/Lib.Net.Http.WebPush.PushServiceClient.html) class is internally holding an instance of `HttpClient` class. As one can read in documentation:
8 |
9 | > HttpClient is intended to be instantiated once and re-used throughout the life of an application. Instantiating an HttpClient class for every request will exhaust the number of sockets available under heavy loads.
10 |
11 | Because of that (in order to avoid Improper Instantiation antipattern) a shared singleton instance of `PushServiceClient` should be created or a pool of reusable instances should be used.
12 |
13 | ## VAPID Tokens Caching
14 |
15 | Generating *VAPID* tokens requires expensive cryptography. The structure of tokens allows for them to be cached per *Audience* (which means by *Push Service*) and *Application Server Keys* pair (for the token expiration period). This library provides such possibility through [`IVapidTokenCache`](../api/Lib.Net.Http.WebPush.Authentication.IVapidTokenCache.html). If an implementation of this interface will be provided to [`VapidAuthentication`](../api/Lib.Net.Http.WebPush.Authentication.VapidAuthentication.html) instance, it will result in tokens being cached.
16 |
17 | Below is a sample implementation which uses [ASP.NET Core in-memory caching](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory).
18 |
19 | ```cs
20 | public class MemoryVapidTokenCache : IVapidTokenCache
21 | {
22 | private readonly IMemoryCache _memoryCache;
23 |
24 | public MemoryVapidTokenCache(IMemoryCache memoryCache)
25 | {
26 | _memoryCache = memoryCache;
27 | }
28 |
29 | public string Get(string audience)
30 | {
31 | if (!_memoryCache.TryGetValue(audience, out string token))
32 | {
33 | token = null;
34 | }
35 |
36 | return token;
37 | }
38 |
39 | public void Put(string audience, DateTimeOffset expiration, string token)
40 | {
41 | _memoryCache.Set(audience, token, expiration);
42 | }
43 | }
44 | ```
45 |
46 | The usage of *Audience* as cache key means that intended context of this interface is a single *Application Server Keys* pair. If application is handling multiple *Application Server Keys* pairs it should provide a separate implementation for every pair and make sure those doesn't clash.
--------------------------------------------------------------------------------
/docs/DocFx.Net.Http.WebPush/docfx.json:
--------------------------------------------------------------------------------
1 | {
2 | "metadata": [
3 | {
4 | "src": [
5 | {
6 | "files": [
7 | "src/Lib.Net.Http.WebPush/Lib.Net.Http.WebPush.csproj",
8 | "src/Lib.AspNetCore.WebPush/Lib.AspNetCore.WebPush.csproj",
9 | "src/Lib.Azure.WebJobs.Extensions.WebPush/Lib.Azure.WebJobs.Extensions.WebPush.csproj",
10 | "src/Lib.Azure.Functions.Worker.Extensions.WebPush/Lib.Azure.Functions.Worker.Extensions.WebPush.csproj"
11 | ],
12 | "exclude": [ "**/bin/**", "**/obj/**" ],
13 | "src": "../.."
14 | }
15 | ],
16 | "dest": "api",
17 | "properties": {
18 | "TargetFramework": "net6.0"
19 | }
20 | }
21 | ],
22 | "build": {
23 | "content": [
24 | {
25 | "files": [
26 | "api/*.yml",
27 | "toc.md",
28 | "index.md",
29 | "articles/performance-considerations.md",
30 | "articles/aspnetcore-integration.md",
31 | "articles/azure-functions-integration.md"
32 | ]
33 | }
34 | ],
35 | "resource": [
36 | {
37 | "files": [
38 | "resources/svg/logo.svg",
39 | "resources/ico/favicon.ico"
40 | ]
41 | }
42 | ],
43 | "dest": "wwwroot",
44 | "globalMetadata": {
45 | "_appTitle": "Lib.Net.Http.WebPush",
46 | "_appFooter": "Copyright © 2018 - 2025 Tomasz Pęczek",
47 | "_appLogoPath": "resources/svg/logo.svg",
48 | "_appFaviconPath": "resources/ico/favicon.ico",
49 | "_disableBreadcrumb": true,
50 | "_disableAffix": true,
51 | "_disableContribution": true
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/docs/DocFx.Net.Http.WebPush/index.md:
--------------------------------------------------------------------------------
1 | # Lib.Net.Http.WebPush
2 |
3 | Lib.Net.Http.WebPush is a library which provides a [*Web Push Protocol*](https://tools.ietf.org/html/rfc8030) based client for *Push Service*. It provides support for [Voluntary Application Server Identification (*VAPID*) for Web Push](https://tools.ietf.org/html/rfc8292) and [Message Encryption for Web Push](https://tools.ietf.org/html/rfc8291).
4 |
5 | Lib.AspNetCore.WebPush is a library which provides ASP.NET Core extensions for Web Push Protocol based client for Push Service.
6 |
7 | Lib.Azure.WebJobs.Extensions.WebPush is a library which provides [Azure Functions](https://functions.azure.com/) in-process model and [Azure WebJobs](https://docs.microsoft.com/en-us/azure/app-service/web-sites-create-web-jobs) binding extensions for Web Push Protocol based client for Push Service.
8 |
9 | Lib.Azure.Functions.Worker.Extensions.WebPush is a library which provides [Azure Functions](https://functions.azure.com/) isolated worker model extensions for Web Push Protocol based client for Push Service.
10 |
11 | ## Installation
12 |
13 | You can install [Lib.Net.Http.WebPush](https://www.nuget.org/packages/Lib.Net.Http.WebPush), [Lib.AspNetCore.WebPush](https://www.nuget.org/packages/Lib.AspNetCore.WebPush), [Lib.Azure.WebJobs.Extensions.WebPush](https://www.nuget.org/packages/Lib.Azure.WebJobs.Extensions.WebPush), and [Lib.Azure.Functions.Worker.Extensions.WebPush](https://www.nuget.org/packages/Lib.Azure.Functions.Worker.Extensions.WebPush) from NuGet.
14 |
15 | ```
16 | PM> Install-Package Lib.Net.Http.WebPush
17 | ```
18 |
19 | ```
20 | PM> Install-Package Lib.AspNetCore.WebPush
21 | ```
22 |
23 | ```
24 | PM> Install-Package Lib.Azure.WebJobs.Extensions.WebPush
25 | ```
26 |
27 | ```
28 | PM> Install-Package Lib.Azure.Functions.Worker.Extensions.WebPush
29 | ```
30 |
31 | ## Demos
32 |
33 | There are several demo projects available:
34 | - [Web Push Notifications in ASP.NET Core Web Application](https://github.com/tpeczek/Demo.AspNetCore.PushNotifications)
35 | - [Web Push Notifications in Azure Functions](https://github.com/tpeczek/Demo.Azure.Funtions.PushNotifications)
36 |
37 | ## Additional Resources
38 |
39 | There is a "Push Notifications and ASP.NET Core" series which provides a lot of information about *Push API*, *Web Push Protocol* and internals of this library:
40 |
41 | - [Push Notifications and ASP.NET Core - Part 1 (Push API)](https://www.tpeczek.com/2017/12/push-notifications-and-aspnet-core-part.html)
42 | - [Push Notifications and ASP.NET Core - Part 2 (Requesting Delivery)](https://www.tpeczek.com/2018/01/push-notifications-and-aspnet-core-part.html)
43 | - [Push Notifications and ASP.NET Core - Part 3 (Replacing Messages & Urgency)](https://www.tpeczek.com/2018/01/push-notifications-and-aspnet-core-part_18.html)
44 | - [Push Notifications and ASP.NET Core - Part 5 (Special Cases)](https://www.tpeczek.com/2019/02/push-notifications-and-aspnet-core-part.html)
45 | - [Scaling Web Push Notifications with Azure Functions](https://www.tpeczek.com/2019/02/scaling-web-push-notifications-with.html)
46 |
47 | ## Donating
48 |
49 | My blog and open source projects are result of my passion for software development, but they require a fair amount of my personal time. If you got value from any of the content I create, then I would appreciate your support by sponsoring me.
50 |
51 |
52 |
--------------------------------------------------------------------------------
/docs/DocFx.Net.Http.WebPush/resources/ico/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpeczek/Lib.Net.Http.WebPush/2d84bc3ef915223491b5417c205be596064ec4a8/docs/DocFx.Net.Http.WebPush/resources/ico/favicon.ico
--------------------------------------------------------------------------------
/docs/DocFx.Net.Http.WebPush/resources/svg/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/docs/DocFx.Net.Http.WebPush/toc.md:
--------------------------------------------------------------------------------
1 | # [Introduction](index.md)
2 |
3 | # [Performance Considerations](articles/performance-considerations.md)
4 |
5 | # [ASP.NET Core Integration](articles/aspnetcore-integration.md)
6 |
7 | # [Azure Functions Integration](articles/azure-functions-integration.md)
8 |
9 | # [API Reference](api/Lib.Net.Http.WebPush.html)
--------------------------------------------------------------------------------
/src/Lib.AspNetCore.WebPush/Caching/DistributedVapidTokenCache.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Extensions.Caching.Distributed;
3 | using Lib.Net.Http.WebPush.Authentication;
4 |
5 | namespace Lib.AspNetCore.WebPush.Caching
6 | {
7 | internal class DistributedVapidTokenCache : IVapidTokenCache
8 | {
9 | private readonly IDistributedCache _distributedCache;
10 |
11 | public DistributedVapidTokenCache(IDistributedCache distributedCache)
12 | {
13 | _distributedCache = distributedCache;
14 | }
15 |
16 | public string Get(string audience)
17 | {
18 | return _distributedCache.GetString(audience);
19 | }
20 |
21 | public void Put(string audience, DateTimeOffset expiration, string token)
22 | {
23 | DistributedCacheEntryOptions cacheEntryOptions = new DistributedCacheEntryOptions()
24 | .SetAbsoluteExpiration(expiration);
25 |
26 | _distributedCache.SetString(audience, token, cacheEntryOptions);
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Lib.AspNetCore.WebPush/Caching/MemoryVapidTokenCache.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Extensions.Caching.Memory;
3 | using Lib.Net.Http.WebPush.Authentication;
4 |
5 | namespace Lib.AspNetCore.WebPush.Caching
6 | {
7 | internal class MemoryVapidTokenCache : IVapidTokenCache
8 | {
9 | private readonly IMemoryCache _memoryCache;
10 |
11 | public MemoryVapidTokenCache(IMemoryCache memoryCache)
12 | {
13 | _memoryCache = memoryCache;
14 | }
15 |
16 | public string Get(string audience)
17 | {
18 | if (!_memoryCache.TryGetValue(audience, out string token))
19 | {
20 | token = null;
21 | }
22 |
23 | return token;
24 | }
25 |
26 | public void Put(string audience, DateTimeOffset expiration, string token)
27 | {
28 | _memoryCache.Set(audience, token, expiration);
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Lib.AspNetCore.WebPush/Lib.AspNetCore.WebPush.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | This package contains ASP.NET Core extensions for Web Push Protocol based client for Push Service.
4 | AspNetCore WebPush
5 | Copyright © 2019 - 2025 Tomasz Pęczek
6 | 2.2.2
7 | Tomasz Pęczek
8 | netcoreapp3.1;net5.0;net6.0;net461
9 | Lib.AspNetCore.WebPush
10 | Lib.AspNetCore.WebPush
11 | Lib.AspNetCore.WebPush
12 | aspnetcore;webpush
13 | https://github.com/tpeczek/Lib.Net.Http.WebPush
14 | MIT
15 | README.md
16 | git
17 | git://github.com/tpeczek/Lib.Net.Http.WebPush
18 | true
19 | true
20 | true
21 | true
22 | true
23 | true
24 | latest
25 | true
26 | Lib.AspNetCore.WebPush.snk
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/Lib.AspNetCore.WebPush/Lib.AspNetCore.WebPush.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpeczek/Lib.Net.Http.WebPush/2d84bc3ef915223491b5417c205be596064ec4a8/src/Lib.AspNetCore.WebPush/Lib.AspNetCore.WebPush.snk
--------------------------------------------------------------------------------
/src/Lib.AspNetCore.WebPush/PushServiceClientOptions.cs:
--------------------------------------------------------------------------------
1 | using Lib.Net.Http.WebPush;
2 | using Lib.Net.Http.WebPush.Authentication;
3 |
4 | namespace Lib.AspNetCore.WebPush
5 | {
6 | ///
7 | /// The options for
8 | ///
9 | public class PushServiceClientOptions
10 | {
11 | #region Properties
12 | ///
13 | /// Gets or sets the contact information for the application server.
14 | ///
15 | public string Subject { get; set; }
16 |
17 | ///
18 | /// Gets or sets the Application Server Public Key.
19 | ///
20 | public string PublicKey { get; set; }
21 |
22 | ///
23 | /// Gets or sets the Application Server Private Key.
24 | ///
25 | public string PrivateKey { get; set; }
26 |
27 | ///
28 | /// Gets or sets the time after which the authentication token expires (in seconds).
29 | ///
30 | public int? Expiration { get; set; }
31 |
32 | ///
33 | /// Gets or sets the default to be used.
34 | ///
35 | public VapidAuthenticationScheme DefaultAuthenticationScheme { get; set; } = VapidAuthenticationScheme.Vapid;
36 |
37 | ///
38 | /// Gets or sets the value indicating if client should automatically attempt to retry in case of 429 Too Many Requests.
39 | ///
40 | public bool AutoRetryAfter { get; set; } = true;
41 |
42 | ///
43 | /// Gets or sets the value indicating the maximum number of automatic attempts to retry in case of 429 Too Many Requests (<= 0 means unlimited).
44 | ///
45 | public int MaxRetriesAfter { get; set; } = 0;
46 |
47 | ///
48 | /// Gets or sets the default time (in seconds) for which the message should be retained by push service. It will be used when is not set.
49 | ///
50 | public int? DefaultTimeToLive { get; set; }
51 | #endregion
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Lib.AspNetCore.WebPush/PushServiceClientServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using Microsoft.Extensions.Options;
4 | using Microsoft.Extensions.Caching.Memory;
5 | using Microsoft.Extensions.Caching.Distributed;
6 | using Lib.Net.Http.WebPush;
7 | using Lib.AspNetCore.WebPush;
8 | using Lib.Net.Http.WebPush.Authentication;
9 | using Lib.AspNetCore.WebPush.Caching;
10 |
11 | namespace Microsoft.Extensions.DependencyInjection
12 | {
13 | ///
14 | /// Extensions methods to configure an for .
15 | ///
16 | public static class PushServiceClientServiceCollectionExtensions
17 | {
18 | private const string HTTP_CLIENT_NAME = "Lib.AspNetCore.WebPush";
19 |
20 | private class VapidAuthenticationProvider : IDisposable
21 | {
22 | public VapidAuthentication VapidAuthentication { get; }
23 |
24 | public VapidAuthenticationProvider(VapidAuthentication vapidAuthentication)
25 | {
26 | VapidAuthentication = vapidAuthentication;
27 | }
28 |
29 | public void Dispose()
30 | {
31 | VapidAuthentication?.Dispose();
32 | }
33 | }
34 |
35 | ///
36 | /// Adds the based implementation of .
37 | ///
38 | /// The .
39 | /// The .
40 | public static IServiceCollection AddMemoryVapidTokenCache(this IServiceCollection services)
41 | {
42 | return services.AddSingleton();
43 | }
44 |
45 | ///
46 | /// Adds the based implementation of .
47 | ///
48 | /// The .
49 | /// The .
50 | public static IServiceCollection AddDistributedVapidTokenCache(this IServiceCollection services)
51 | {
52 | return services.AddSingleton();
53 | }
54 |
55 | ///
56 | /// Adds the and related services to the .
57 | ///
58 | /// The .
59 | /// The .
60 | public static IServiceCollection AddPushServiceClient(this IServiceCollection services)
61 | {
62 | if (services == null)
63 | {
64 | throw new ArgumentNullException(nameof(services));
65 | }
66 |
67 | services.AddHttpClient(HTTP_CLIENT_NAME);
68 |
69 | services.AddSingleton(serviceProvider =>
70 | {
71 | VapidAuthentication vapidAuthentication = null;
72 |
73 | IOptions options = serviceProvider.GetRequiredService>();
74 | if (options.Value != null)
75 | {
76 | PushServiceClientOptions optionsValue = options.Value;
77 |
78 | if (!String.IsNullOrWhiteSpace(optionsValue.PrivateKey) && !String.IsNullOrWhiteSpace(optionsValue.PublicKey))
79 | {
80 | vapidAuthentication = new VapidAuthentication(optionsValue.PublicKey, optionsValue.PrivateKey)
81 | {
82 | Subject = optionsValue.Subject
83 | };
84 |
85 | if (optionsValue.Expiration.HasValue)
86 | {
87 | vapidAuthentication.Expiration = optionsValue.Expiration.Value;
88 | }
89 |
90 | vapidAuthentication.TokenCache = serviceProvider.GetService();
91 | }
92 | }
93 |
94 | return new VapidAuthenticationProvider(vapidAuthentication);
95 | });
96 |
97 | services.AddTransient(serviceProvider =>
98 | {
99 | IHttpClientFactory clientFactory = serviceProvider.GetRequiredService();
100 | IOptions options = serviceProvider.GetRequiredService>();
101 |
102 | PushServiceClient pushServiceClient = new PushServiceClient(clientFactory.CreateClient(HTTP_CLIENT_NAME));
103 | if (options.Value != null)
104 | {
105 | PushServiceClientOptions optionsValue = options.Value;
106 |
107 | VapidAuthenticationProvider vapidAuthenticationProvider = serviceProvider.GetRequiredService();
108 | if (vapidAuthenticationProvider.VapidAuthentication != null)
109 | {
110 | pushServiceClient.DefaultAuthentication = vapidAuthenticationProvider.VapidAuthentication;
111 | }
112 |
113 | pushServiceClient.DefaultAuthenticationScheme = optionsValue.DefaultAuthenticationScheme;
114 |
115 | if (optionsValue.DefaultTimeToLive.HasValue)
116 | {
117 | pushServiceClient.DefaultTimeToLive = optionsValue.DefaultTimeToLive.Value;
118 | }
119 |
120 | pushServiceClient.AutoRetryAfter = optionsValue.AutoRetryAfter;
121 | pushServiceClient.MaxRetriesAfter = optionsValue.MaxRetriesAfter;
122 | }
123 |
124 | return pushServiceClient;
125 | });
126 |
127 | return services;
128 | }
129 |
130 | ///
131 | /// Adds the and related services to the .
132 | ///
133 | /// The .
134 | /// Used to configure the .
135 | /// The .
136 | public static IServiceCollection AddPushServiceClient(this IServiceCollection services, Action configureOptions)
137 | {
138 | if (services == null)
139 | {
140 | throw new ArgumentNullException(nameof(services));
141 | }
142 |
143 | if (configureOptions == null)
144 | {
145 | throw new ArgumentNullException(nameof(configureOptions));
146 | }
147 |
148 | services.AddPushServiceClient();
149 | services.Configure(configureOptions);
150 |
151 | return services;
152 | }
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/Lib.Azure.Functions.Worker.Extensions.WebPush/Constants.cs:
--------------------------------------------------------------------------------
1 | namespace Lib.Azure.Functions.Worker.Extensions.WebPush
2 | {
3 | internal static class Constants
4 | {
5 | internal const string PUSH_SERVICE_EXTENSION_NAME = "PushService";
6 |
7 | internal const string JSON_CONTENT_TYPE = "application/json";
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Lib.Azure.Functions.Worker.Extensions.WebPush/FunctionsWorkerApplicationBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Azure.Functions.Worker;
3 | using Microsoft.Extensions.DependencyInjection;
4 |
5 | namespace Lib.Azure.Functions.Worker.Extensions.WebPush
6 | {
7 | internal static class FunctionsWorkerApplicationBuilderExtensions
8 | {
9 | public static IFunctionsWorkerApplicationBuilder ConfigurePushServiceExtension(this IFunctionsWorkerApplicationBuilder builder)
10 | {
11 | ArgumentNullException.ThrowIfNull(builder, nameof(builder));
12 |
13 | builder.Services.AddHttpClient();
14 |
15 | return builder;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Lib.Azure.Functions.Worker.Extensions.WebPush/Lib.Azure.Functions.Worker.Extensions.WebPush.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | This package contains Azure Functions isolated worker model extensions for Web Push Protocol based client for Push Service.
4 | Azure AzureFunctions IsolatedWorker WebPush
5 | Copyright © 2024 - 2025 Tomasz Pęczek
6 | 1.0.1
7 | Tomasz Pęczek
8 | net6.0
9 | Lib.Azure.Functions.Worker.Extensions.WebPush
10 | Lib.Azure.Functions.Worker.Extensions.WebPush
11 | Lib.Azure.Functions.Worker.Extensions.WebPush
12 | azure;azurefunctions;isolatedworker;webpush
13 | https://github.com/tpeczek/Lib.Net.Http.WebPush
14 | MIT
15 | README.md
16 | git
17 | git://github.com/tpeczek/Lib.Net.Http.WebPush
18 | true
19 | true
20 | true
21 | true
22 | true
23 | true
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/Lib.Azure.Functions.Worker.Extensions.WebPush/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | [assembly: Microsoft.Azure.Functions.Worker.Extensions.Abstractions.ExtensionInformation("Lib.Azure.WebJobs.Extensions.WebPush", "1.5.0")]
2 |
--------------------------------------------------------------------------------
/src/Lib.Azure.Functions.Worker.Extensions.WebPush/PushServiceConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Threading.Tasks;
4 | using Microsoft.Azure.Functions.Worker.Core;
5 | using Microsoft.Azure.Functions.Worker.Converters;
6 | using Microsoft.Azure.Functions.Worker.Extensions.Abstractions;
7 | using Lib.Net.Http.WebPush;
8 | using Lib.Net.Http.WebPush.Authentication;
9 |
10 | namespace Lib.Azure.Functions.Worker.Extensions.WebPush
11 | {
12 | [SupportsDeferredBinding]
13 | [SupportedTargetType(typeof(PushServiceClient))]
14 | internal class PushServiceConverter : IInputConverter
15 | {
16 | private static readonly Type TYPE_OF_PUSH_SERVICE_CLIENT = typeof(PushServiceClient);
17 |
18 | private readonly IHttpClientFactory _httpClientFactory;
19 |
20 | public PushServiceConverter(IHttpClientFactory httpClientFactory)
21 | {
22 | _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
23 | }
24 |
25 | public ValueTask ConvertAsync(ConverterContext context)
26 | {
27 | try
28 | {
29 | if (!CanConvert(context))
30 | {
31 | return new(ConversionResult.Unhandled());
32 | }
33 |
34 | var modelBindingData = context?.Source as ModelBindingData;
35 |
36 | PushServiceClient pushServiceClient = CreatePushServiceClient(modelBindingData);
37 |
38 | return new(ConversionResult.Success(pushServiceClient));
39 | }
40 | catch (Exception ex)
41 | {
42 | return new(ConversionResult.Failed(ex));
43 | }
44 | }
45 |
46 | private static bool CanConvert(ConverterContext context)
47 | {
48 | ArgumentNullException.ThrowIfNull(context, nameof(context));
49 |
50 | if (context.TargetType != TYPE_OF_PUSH_SERVICE_CLIENT)
51 | {
52 | return false;
53 | }
54 |
55 | if (context.Source is not ModelBindingData bindingData)
56 | {
57 | return false;
58 | }
59 |
60 | if (bindingData.Source is not Constants.PUSH_SERVICE_EXTENSION_NAME)
61 | {
62 | throw new InvalidOperationException($"Unexpected binding source '{bindingData.Source}'. Only '{Constants.PUSH_SERVICE_EXTENSION_NAME}' is supported.");
63 | }
64 |
65 | if (bindingData.ContentType is not Constants.JSON_CONTENT_TYPE)
66 | {
67 | throw new InvalidOperationException($"Unexpected content-type '{bindingData.ContentType}'. Only '{Constants.JSON_CONTENT_TYPE}' is supported.");
68 | }
69 |
70 | return true;
71 | }
72 |
73 | private PushServiceClient CreatePushServiceClient(ModelBindingData bindingData)
74 | {
75 | var pushServiceModelBindingDataContent = bindingData.Content.ToObjectFromJson();
76 |
77 | PushServiceClient pushServiceClient = new PushServiceClient(_httpClientFactory.CreateClient())
78 | {
79 | DefaultAuthentication = new VapidAuthentication(pushServiceModelBindingDataContent.PublicKey, pushServiceModelBindingDataContent.PrivateKey)
80 | {
81 | Subject = pushServiceModelBindingDataContent.Subject
82 | },
83 | AutoRetryAfter = pushServiceModelBindingDataContent.AutoRetryAfter,
84 | MaxRetriesAfter = pushServiceModelBindingDataContent.MaxRetriesAfter
85 | };
86 |
87 | if (pushServiceModelBindingDataContent.DefaultTimeToLive.HasValue)
88 | {
89 | pushServiceClient.DefaultTimeToLive = pushServiceModelBindingDataContent.DefaultTimeToLive.Value;
90 | }
91 |
92 | return pushServiceClient;
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/Lib.Azure.Functions.Worker.Extensions.WebPush/PushServiceExtensionStartup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Azure.Functions.Worker;
3 | using Microsoft.Azure.Functions.Worker.Core;
4 |
5 | [assembly: WorkerExtensionStartup(typeof(Lib.Azure.Functions.Worker.Extensions.WebPush.PushServiceExtensionStartup))]
6 |
7 | namespace Lib.Azure.Functions.Worker.Extensions.WebPush
8 | {
9 | ///
10 | /// Class providing a worker extension startup implementation.
11 | ///
12 | public class PushServiceExtensionStartup : WorkerExtensionStartup
13 | {
14 | ///
15 | /// Performs the startup configuration action for Push Service extension.
16 | ///
17 | /// The that can be used to configure the worker extension.
18 | public override void Configure(IFunctionsWorkerApplicationBuilder applicationBuilder)
19 | {
20 | ArgumentNullException.ThrowIfNull(applicationBuilder, nameof(applicationBuilder));
21 |
22 | applicationBuilder.ConfigurePushServiceExtension();
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Lib.Azure.Functions.Worker.Extensions.WebPush/PushServiceInputAttribute.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Azure.Functions.Worker.Converters;
2 | using Microsoft.Azure.Functions.Worker.Extensions.Abstractions;
3 |
4 | namespace Lib.Azure.Functions.Worker.Extensions.WebPush
5 | {
6 | ///
7 | /// Attribute used to configure a parameter as the input target for the PushService binding.
8 | ///
9 | ///
10 | /// The method parameter type can be one of the following:
11 | ///
12 | ///
13 | ///
14 | ///
15 | [InputConverter(typeof(PushServiceConverter))]
16 | [ConverterFallbackBehavior(ConverterFallbackBehavior.Default)]
17 | public sealed class PushServiceInputAttribute : InputBindingAttribute
18 | {
19 | ///
20 | /// The application server public key.
21 | ///
22 | public string PublicKeySetting { get; set; }
23 |
24 | ///
25 | /// The application server private key.
26 | ///
27 | public string PrivateKeySetting { get; set; }
28 |
29 | ///
30 | /// The contact information for the application server.
31 | ///
32 | public string SubjectSetting { get; set; }
33 |
34 | ///
35 | /// The value indicating if client should automatically attempt to retry in case of 429 Too Many Requests.
36 | ///
37 | public bool AutoRetryAfter { get; set; } = true;
38 |
39 | ///
40 | /// The value indicating the maximum number of automatic attempts to retry in case of 429 Too Many Requests (<= 0 means unlimited).
41 | ///
42 | public int MaxRetriesAfter { get; set; } = 0;
43 |
44 | ///
45 | /// The default time (in seconds) for which the message should be retained by push service. It will be used when is not set.
46 | ///
47 | public int? DefaultTimeToLive { get; set; }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Lib.Azure.Functions.Worker.Extensions.WebPush/PushServiceModelBindingDataContent.cs:
--------------------------------------------------------------------------------
1 | namespace Lib.Azure.Functions.Worker.Extensions.WebPush
2 | {
3 | internal class PushServiceModelBindingDataContent
4 | {
5 | public string PublicKey { get; set; }
6 |
7 | public string PrivateKey { get; set; }
8 |
9 | public string Subject { get; set; }
10 |
11 | public bool AutoRetryAfter { get; set; }
12 |
13 | public int MaxRetriesAfter { get; set; }
14 |
15 | public int? DefaultTimeToLive { get; set; }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Lib.Azure.WebJobs.Extensions.WebPush/Bindings/PushServiceAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Azure.WebJobs.Description;
3 | using Lib.Net.Http.WebPush;
4 |
5 | namespace Lib.Azure.WebJobs.Extensions.WebPush.Bindings
6 | {
7 | ///
8 | /// Attribute used to bind to a PushService.
9 | ///
10 | ///
11 | /// The method parameter type can be one of the following:
12 | ///
13 | ///
14 | ///
15 | ///
16 | [Binding]
17 | [AttributeUsage(AttributeTargets.Parameter)]
18 | public sealed class PushServiceAttribute : Attribute
19 | {
20 | ///
21 | /// The application server public key.
22 | ///
23 | [AppSetting]
24 | public string PublicKeySetting { get; set; }
25 |
26 | ///
27 | /// The application server private key.
28 | ///
29 | [AppSetting]
30 | public string PrivateKeySetting { get; set; }
31 |
32 | ///
33 | /// The contact information for the application server.
34 | ///
35 | [AppSetting]
36 | public string SubjectSetting { get; set; }
37 |
38 | ///
39 | /// The value indicating if client should automatically attempt to retry in case of 429 Too Many Requests.
40 | ///
41 | public bool AutoRetryAfter { get; set; } = true;
42 |
43 | ///
44 | /// The value indicating the maximum number of automatic attempts to retry in case of 429 Too Many Requests (<= 0 means unlimited).
45 | ///
46 | public int MaxRetriesAfter { get; set; } = 0;
47 |
48 | ///
49 | /// The default time (in seconds) for which the message should be retained by push service. It will be used when is not set.
50 | ///
51 | public int? DefaultTimeToLive { get; set; }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Lib.Azure.WebJobs.Extensions.WebPush/Bindings/PushServiceClientConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using Microsoft.Azure.WebJobs;
4 | using Lib.Net.Http.WebPush;
5 | using Lib.Net.Http.WebPush.Authentication;
6 |
7 | namespace Lib.Azure.WebJobs.Extensions.WebPush.Bindings
8 | {
9 | internal class PushServiceClientConverter : IConverter
10 | {
11 | private readonly PushServiceOptions _options;
12 | private readonly IHttpClientFactory _httpClientFactory;
13 |
14 | public PushServiceClientConverter(PushServiceOptions options, IHttpClientFactory httpClientFactory)
15 | {
16 | _options = options;
17 | _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
18 | }
19 |
20 | public PushServiceClient Convert(PushServiceAttribute attribute)
21 | {
22 | PushServiceClient pushServiceClient = new PushServiceClient(_httpClientFactory.CreateClient())
23 | {
24 | DefaultAuthentication = new VapidAuthentication(ResolveAuthenticationProperty(attribute.PublicKeySetting, _options?.PublicKey), ResolveAuthenticationProperty(attribute.PrivateKeySetting, _options?.PrivateKey))
25 | {
26 | Subject = ResolveAuthenticationProperty(attribute.SubjectSetting, _options?.Subject)
27 | },
28 | AutoRetryAfter = attribute.AutoRetryAfter,
29 | MaxRetriesAfter = attribute.MaxRetriesAfter
30 | };
31 |
32 | if (attribute.DefaultTimeToLive.HasValue)
33 | {
34 | pushServiceClient.DefaultTimeToLive = attribute.DefaultTimeToLive.Value;
35 | }
36 |
37 | return pushServiceClient;
38 | }
39 |
40 | private static string ResolveAuthenticationProperty(string attributeValue, string optionsValue)
41 | {
42 | if (!String.IsNullOrEmpty(attributeValue))
43 | {
44 | return attributeValue;
45 | }
46 |
47 | return optionsValue;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Lib.Azure.WebJobs.Extensions.WebPush/Bindings/PushServiceParameterBindingDataContent.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Lib.Azure.WebJobs.Extensions.WebPush.Bindings
4 | {
5 | internal class PushServiceParameterBindingDataContent
6 | {
7 | public string PublicKey { get; set; }
8 |
9 | public string PrivateKey { get; set; }
10 |
11 | public string Subject { get; set; }
12 |
13 | public bool AutoRetryAfter { get; set; }
14 |
15 | public int MaxRetriesAfter { get; set; }
16 |
17 | public int? DefaultTimeToLive { get; set; }
18 |
19 | public PushServiceParameterBindingDataContent(PushServiceAttribute attribute, PushServiceOptions options)
20 | {
21 | PublicKey = !String.IsNullOrEmpty(attribute.PublicKeySetting) ? attribute.PublicKeySetting : options?.PublicKey;
22 | PrivateKey = !String.IsNullOrEmpty(attribute.PrivateKeySetting) ? attribute.PrivateKeySetting : options?.PrivateKey;
23 | Subject = !String.IsNullOrEmpty(attribute.SubjectSetting) ? attribute.SubjectSetting : options?.Subject;
24 | AutoRetryAfter = attribute.AutoRetryAfter;
25 | MaxRetriesAfter = attribute.MaxRetriesAfter;
26 | DefaultTimeToLive = attribute.DefaultTimeToLive;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Lib.Azure.WebJobs.Extensions.WebPush/Config/PushServiceExtensionConfigProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using Microsoft.Extensions.Options;
4 | using Microsoft.Azure.WebJobs;
5 | using Microsoft.Azure.WebJobs.Description;
6 | using Microsoft.Azure.WebJobs.Host.Config;
7 | using Lib.Azure.WebJobs.Extensions.WebPush.Bindings;
8 | using Lib.Net.Http.WebPush;
9 |
10 | namespace Lib.Azure.WebJobs.Extensions.WebPush.Config
11 | {
12 | [Extension(Constants.PushServiceExtensionName)]
13 | internal class PushServiceExtensionConfigProvider : IExtensionConfigProvider
14 | {
15 | private readonly PushServiceOptions _options;
16 | private readonly IHttpClientFactory _httpClientFactory;
17 |
18 | public PushServiceExtensionConfigProvider(IOptions options, IHttpClientFactory httpClientFactory)
19 | {
20 | _options = options.Value;
21 | _httpClientFactory = httpClientFactory;
22 | }
23 |
24 | public void Initialize(ExtensionConfigContext context)
25 | {
26 | if (context == null)
27 | {
28 | throw new ArgumentNullException(nameof(context));
29 | }
30 |
31 | //PushServiceClient Bindings
32 | var bindingAttributeBindingRule = context.AddBindingRule();
33 | bindingAttributeBindingRule.AddValidator(ValidateVapidAuthentication);
34 |
35 | bindingAttributeBindingRule.BindToInput(typeof(PushServiceClientConverter), _options, _httpClientFactory);
36 | bindingAttributeBindingRule.BindToInput(CreateParameterBindingData);
37 | }
38 |
39 | private void ValidateVapidAuthentication(PushServiceAttribute attribute, Type paramType)
40 | {
41 | if (String.IsNullOrEmpty(_options.PublicKey) && String.IsNullOrEmpty(attribute.PublicKeySetting))
42 | {
43 | string attributeProperty = $"{nameof(PushServiceAttribute)}.{nameof(PushServiceAttribute.PublicKeySetting)}";
44 | string optionsProperty = $"{nameof(PushServiceOptions)}.{nameof(PushServiceOptions.PublicKey)}";
45 | throw new InvalidOperationException($"The application server public key must be set either via the {attributeProperty} property or via {optionsProperty}.");
46 | }
47 |
48 | if (String.IsNullOrEmpty(_options.PrivateKey) && String.IsNullOrEmpty(attribute.PrivateKeySetting))
49 | {
50 | string attributeProperty = $"{nameof(PushServiceAttribute)}.{nameof(PushServiceAttribute.PrivateKeySetting)}";
51 | string optionsProperty = $"{nameof(PushServiceOptions)}.{nameof(PushServiceOptions.PrivateKey)}";
52 | throw new InvalidOperationException($"The application server private key must be set either via the {attributeProperty} property or via {optionsProperty}.");
53 | }
54 | }
55 |
56 | internal ParameterBindingData CreateParameterBindingData(PushServiceAttribute attribute)
57 | {
58 | var pushServiceParameterBindingData = new PushServiceParameterBindingDataContent(attribute, _options);
59 | var pushServiceParameterBinaryData = new BinaryData(pushServiceParameterBindingData);
60 | var parameterBindingData = new ParameterBindingData("1.0", Constants.PushServiceExtensionName, pushServiceParameterBinaryData, "application/json");
61 |
62 | return parameterBindingData;
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Lib.Azure.WebJobs.Extensions.WebPush/Constants.cs:
--------------------------------------------------------------------------------
1 | namespace Lib.Azure.WebJobs.Extensions.WebPush
2 | {
3 | internal static class Constants
4 | {
5 | internal const string PushServiceExtensionName = "PushService";
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/Lib.Azure.WebJobs.Extensions.WebPush/Lib.Azure.WebJobs.Extensions.WebPush.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | This package contains Azure Functions and Azure WebJobs binding extensions for Web Push Protocol based client for Push Service.
4 | Azure WebJobs AzureFunctions WebPush
5 | Copyright © 2019 - 2025 Tomasz Pęczek
6 | 1.5.1
7 | Tomasz Pęczek
8 | netstandard2.0
9 | Lib.Azure.WebJobs.Extensions.WebPush
10 | Lib.Azure.WebJobs.Extensions.WebPush
11 | Lib.Azure.WebJobs.Extensions.WebPush
12 | azure;webjobs;azurefunctions;webpush
13 | https://github.com/tpeczek/Lib.Net.Http.WebPush
14 | MIT
15 | README.md
16 | git
17 | git://github.com/tpeczek/Lib.Net.Http.WebPush
18 | true
19 | true
20 | true
21 | true
22 | true
23 | true
24 | latest
25 | true
26 | Lib.Azure.WebJobs.Extensions.WebPush.snk
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/Lib.Azure.WebJobs.Extensions.WebPush/Lib.Azure.WebJobs.Extensions.WebPush.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpeczek/Lib.Net.Http.WebPush/2d84bc3ef915223491b5417c205be596064ec4a8/src/Lib.Azure.WebJobs.Extensions.WebPush/Lib.Azure.WebJobs.Extensions.WebPush.snk
--------------------------------------------------------------------------------
/src/Lib.Azure.WebJobs.Extensions.WebPush/PushServiceOptions.cs:
--------------------------------------------------------------------------------
1 | namespace Lib.Azure.WebJobs.Extensions.WebPush
2 | {
3 | ///
4 | /// Options for Push Service binding extensions.
5 | ///
6 | public class PushServiceOptions
7 | {
8 | ///
9 | /// The application server public key.
10 | ///
11 | public string PublicKey { get; set; }
12 |
13 | ///
14 | /// The application server private key.
15 | ///
16 | public string PrivateKey { get; set; }
17 |
18 | ///
19 | /// The contact information for the application server.
20 | ///
21 | public string Subject { get; set; }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Lib.Azure.WebJobs.Extensions.WebPush/PushServiceWebJobsBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Azure.WebJobs;
3 | using Microsoft.Extensions.Http;
4 | using Microsoft.Extensions.Configuration;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Lib.Azure.WebJobs.Extensions.WebPush.Config;
7 |
8 | namespace Lib.Azure.WebJobs.Extensions.WebPush
9 | {
10 | ///
11 | /// The extension methods for Push Service binding extensions.
12 | ///
13 | public static class PushServiceWebJobsBuilderExtensions
14 | {
15 | ///
16 | /// Adds the Push Service binding extensions to the provided .
17 | ///
18 | /// The to configure.
19 | /// An to configure the provided .
20 | public static IWebJobsBuilder AddPushService(this IWebJobsBuilder builder, Action configure)
21 | {
22 | if (builder is null)
23 | {
24 | throw new ArgumentNullException(nameof(builder));
25 | }
26 |
27 | if (configure is null)
28 | {
29 | throw new ArgumentNullException(nameof(configure));
30 | }
31 |
32 | builder.AddPushService();
33 | builder.Services.Configure(configure);
34 |
35 | return builder;
36 | }
37 |
38 | ///
39 | /// Adds the Push Service binding extensions to the provided .
40 | ///
41 | /// The to configure.
42 | public static IWebJobsBuilder AddPushService(this IWebJobsBuilder builder)
43 | {
44 | if (builder is null)
45 | {
46 | throw new ArgumentNullException(nameof(builder));
47 | }
48 |
49 | builder.AddExtension()
50 | .ConfigureOptions((config, path, options) =>
51 | {
52 | config.GetSection(path).Bind(options);
53 | });
54 |
55 | builder.Services.AddHttpClient();
56 | builder.Services.Configure(options => options.SuppressHandlerScope = true);
57 |
58 | return builder;
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Lib.Azure.WebJobs.Extensions.WebPush/PushServiceWebJobsStartup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Azure.WebJobs;
2 | using Microsoft.Azure.WebJobs.Hosting;
3 | using Lib.Azure.WebJobs.Extensions.WebPush;
4 |
5 | [assembly: WebJobsStartup(typeof(PushServiceWebJobsStartup))]
6 |
7 | namespace Lib.Azure.WebJobs.Extensions.WebPush
8 | {
9 | ///
10 | /// Class defining a startup configuration action for Push Service binding extensions, which will be performed as part of host startup.
11 | ///
12 | public class PushServiceWebJobsStartup : IWebJobsStartup
13 | {
14 | ///
15 | /// Performs the startup configuration action for Push Service binding extensions.
16 | ///
17 | /// The that can be used to configure the host.
18 | public void Configure(IWebJobsBuilder builder)
19 | {
20 | builder.AddPushService();
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/Authentication/IVapidTokenCache.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Lib.Net.Http.WebPush.Authentication
4 | {
5 | ///
6 | /// Represents cache for tokens.
7 | ///
8 | public interface IVapidTokenCache
9 | {
10 | ///
11 | /// Puts token into cache.
12 | ///
13 | /// The origin of the push resource (cache key).
14 | /// The token expiration.
15 | /// The token.
16 | void Put(string audience, DateTimeOffset expiration, string token);
17 |
18 | ///
19 | /// Gets token from cache.
20 | ///
21 | /// The origin of the push resource (cache key).
22 | /// The cached token or null if token was not present in cache.
23 | string Get(string audience);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/Authentication/VapidAuthentication.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using System.Globalization;
4 | using Lib.Net.Http.WebPush.Internals;
5 |
6 | namespace Lib.Net.Http.WebPush.Authentication
7 | {
8 | ///
9 | /// Class which provides Voluntary Application Server Identification (VAPID) headers values.
10 | ///
11 | public class VapidAuthentication : IDisposable
12 | {
13 | #region Structures
14 | ///
15 | /// Structure providing values for headers used in case of .
16 | ///
17 | public readonly struct WebPushSchemeHeadersValues
18 | {
19 | ///
20 | /// Gets the parameter.
21 | ///
22 | public string AuthenticationHeaderValueParameter { get; }
23 |
24 | ///
25 | /// Gets the Crypto-Key header value.
26 | ///
27 | public string CryptoKeyHeaderValue { get; }
28 |
29 | internal WebPushSchemeHeadersValues(string authenticationHeaderValueParameter, string cryptoKeyHeaderValue)
30 | : this()
31 | {
32 | AuthenticationHeaderValueParameter = authenticationHeaderValueParameter;
33 | CryptoKeyHeaderValue = cryptoKeyHeaderValue;
34 | }
35 | }
36 | #endregion
37 |
38 | #region Fields
39 | private const string URI_SCHEME_HTTPS = "https";
40 |
41 | private const int DEFAULT_EXPIRATION = 43200;
42 | private const int MAXIMUM_EXPIRATION = 86400;
43 |
44 | private const string JWT_HEADER = "{\"typ\":\"JWT\",\"alg\":\"ES256\"}";
45 | private const string JWT_SEPARATOR = ".";
46 | private const string JWT_BODY_AUDIENCE_PART = "{\"aud\":\"";
47 | private const string JWT_BODY_EXPIRATION_PART = "\",\"exp\":";
48 | private const string JWT_BODY_SUBJECT_PART = ",\"sub\":\"";
49 | private const string JWT_BODY_WITH_SUBJECT_CLOSING = "\"}";
50 | private const string JWT_BODY_WITHOUT_SUBJECT_CLOSING = "}";
51 |
52 | private const string P256ECDSA_PREFIX = "p256ecdsa=";
53 | private const string VAPID_AUTHENTICATION_HEADER_VALUE_PARAMETER_FORMAT = "t={0}, k={1}";
54 |
55 | private string _subject;
56 | private string _publicKey;
57 | private string _privateKey;
58 | private int _relativeExpiration;
59 |
60 | private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0);
61 | private static readonly string _jwtHeaderSegment = UrlBase64Converter.ToUrlBase64String(Encoding.UTF8.GetBytes(JWT_HEADER));
62 |
63 | private ES256Signer _jwtSigner;
64 | #endregion
65 |
66 | #region Properties
67 | ///
68 | /// Gets or sets the contact information for the application server.
69 | ///
70 | public string Subject
71 | {
72 | get { return _subject; }
73 |
74 | set
75 | {
76 | if (!String.IsNullOrWhiteSpace(value))
77 | {
78 | if (!value.StartsWith("mailto:"))
79 | {
80 | if (!Uri.IsWellFormedUriString(value, UriKind.Absolute) || ((new Uri(value)).Scheme != URI_SCHEME_HTTPS))
81 | {
82 | throw new ArgumentException(nameof(Subject), "Subject should include a contact URI for the application server as either a 'mailto: ' (email) or an 'https:' URI");
83 | }
84 | }
85 |
86 | _subject = value;
87 | }
88 | else
89 | {
90 | _subject = null;
91 | }
92 | }
93 | }
94 |
95 | ///
96 | /// Gets or sets the Application Server Public Key.
97 | ///
98 | public string PublicKey
99 | {
100 | get { return _publicKey; }
101 |
102 | set
103 | {
104 | if (String.IsNullOrWhiteSpace(value))
105 | {
106 | throw new ArgumentNullException(nameof(PublicKey));
107 | }
108 |
109 | byte[] decodedPublicKey = UrlBase64Converter.FromUrlBase64String(value);
110 | if (decodedPublicKey.Length != 65)
111 | {
112 | throw new ArgumentException(nameof(PublicKey), "VAPID public key must be 65 bytes long");
113 | }
114 |
115 | _publicKey = value;
116 | }
117 | }
118 |
119 | ///
120 | /// Gets or sets the Application Server Private Key.
121 | ///
122 | public string PrivateKey
123 | {
124 | get { return _privateKey; }
125 |
126 | set
127 | {
128 | if (String.IsNullOrWhiteSpace(value))
129 | {
130 | throw new ArgumentNullException(nameof(PrivateKey));
131 | }
132 |
133 | byte[] decodedPrivateKey = UrlBase64Converter.FromUrlBase64String(value);
134 | if (decodedPrivateKey.Length != 32)
135 | {
136 | throw new ArgumentException(nameof(PrivateKey), "VAPID private key should be 32 bytes long");
137 | }
138 |
139 | _privateKey = value;
140 |
141 | _jwtSigner = new ES256Signer(decodedPrivateKey);
142 | }
143 | }
144 |
145 | ///
146 | /// Gets or sets the time after which the authentication token expires (in seconds).
147 | ///
148 | public int Expiration
149 | {
150 | get { return _relativeExpiration; }
151 |
152 | set
153 | {
154 | if ((value <= 0) || (value > MAXIMUM_EXPIRATION))
155 | {
156 | throw new ArgumentOutOfRangeException(nameof(Expiration), "Expiration must be a number of seconds not longer than 24 hours");
157 | }
158 |
159 | _relativeExpiration = value;
160 | }
161 | }
162 |
163 | ///
164 | /// Gets or sets the token cache.
165 | ///
166 | public IVapidTokenCache TokenCache { get; set; }
167 | #endregion
168 |
169 | #region Constructor
170 | ///
171 | /// Creates new instance of class.
172 | ///
173 | /// The Application Server Public Key.
174 | /// The Application Server Private Key.
175 | public VapidAuthentication(string publicKey, string privateKey)
176 | {
177 | PublicKey = publicKey;
178 | PrivateKey = privateKey;
179 |
180 | _relativeExpiration = DEFAULT_EXPIRATION;
181 | }
182 | #endregion
183 |
184 | #region Methods
185 | ///
186 | /// Gets parameter for .
187 | ///
188 | /// The origin of the push resource.
189 | /// The parameter for .
190 | public string GetVapidSchemeAuthenticationHeaderValueParameter(string audience)
191 | {
192 | return String.Format(VAPID_AUTHENTICATION_HEADER_VALUE_PARAMETER_FORMAT, GetToken(audience), _publicKey);
193 | }
194 |
195 | ///
196 | /// Gets values for headers used in case of .
197 | ///
198 | /// The origin of the push resource.
199 | /// The values for headers used in case of .
200 | public WebPushSchemeHeadersValues GetWebPushSchemeHeadersValues(string audience)
201 | {
202 | return new WebPushSchemeHeadersValues(GetToken(audience), P256ECDSA_PREFIX + _publicKey);
203 | }
204 |
205 | ///
206 | /// Releases all resources used by the current instance of the class.
207 | ///
208 | public void Dispose()
209 | {
210 | _jwtSigner?.Dispose();
211 | }
212 |
213 | private string GetToken(string audience)
214 | {
215 | if (String.IsNullOrWhiteSpace(audience))
216 | {
217 | throw new ArgumentNullException(nameof(audience));
218 | }
219 |
220 | if (!Uri.IsWellFormedUriString(audience, UriKind.Absolute))
221 | {
222 | throw new ArgumentException(nameof(audience), "Audience should be an absolute URL");
223 | }
224 |
225 | string token = TokenCache?.Get(audience);
226 |
227 | if (token == null)
228 | {
229 | DateTime absoluteExpiration = DateTime.UtcNow.AddSeconds(_relativeExpiration);
230 |
231 | token = GenerateToken(audience, absoluteExpiration);
232 |
233 | TokenCache?.Put(audience, absoluteExpiration, token);
234 | }
235 |
236 | return token;
237 | }
238 |
239 | private string GenerateToken(string audience, DateTime absoluteExpiration)
240 | {
241 | string jwtInput = _jwtHeaderSegment + JWT_SEPARATOR + GenerateJwtBodySegment(audience, absoluteExpiration);
242 |
243 | return jwtInput + JWT_SEPARATOR + UrlBase64Converter.ToUrlBase64String(_jwtSigner.GenerateSignature(jwtInput));
244 | }
245 |
246 | private string GenerateJwtBodySegment(string audience, DateTime absoluteExpiration)
247 | {
248 | StringBuilder jwtBodyBuilder = new StringBuilder();
249 |
250 | jwtBodyBuilder.Append(JWT_BODY_AUDIENCE_PART).Append(audience)
251 | .Append(JWT_BODY_EXPIRATION_PART).Append(ToUnixTimeSeconds(absoluteExpiration).ToString(CultureInfo.InvariantCulture));
252 |
253 | if (_subject != null)
254 | {
255 | jwtBodyBuilder.Append(JWT_BODY_SUBJECT_PART).Append(_subject).Append(JWT_BODY_WITH_SUBJECT_CLOSING);
256 | }
257 | else
258 | {
259 | jwtBodyBuilder.Append(JWT_BODY_WITHOUT_SUBJECT_CLOSING);
260 | }
261 |
262 | return UrlBase64Converter.ToUrlBase64String(Encoding.UTF8.GetBytes(jwtBodyBuilder.ToString()));
263 | }
264 |
265 | private static long ToUnixTimeSeconds(DateTime dateTime)
266 | {
267 | TimeSpan unixEpochOffset = dateTime - _unixEpoch;
268 |
269 | return (long)unixEpochOffset.TotalSeconds;
270 | }
271 | #endregion
272 | }
273 | }
274 |
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/Authentication/VapidAuthenticationScheme.cs:
--------------------------------------------------------------------------------
1 | namespace Lib.Net.Http.WebPush.Authentication
2 | {
3 | ///
4 | /// Voluntary Application Server Identification (VAPID) HTTP authentication schemes.
5 | ///
6 | public enum VapidAuthenticationScheme
7 | {
8 | ///
9 | /// The "WebPush" HTTP authentication scheme.
10 | ///
11 | WebPush,
12 | ///
13 | /// The "vapid" HTTP authentication scheme.
14 | ///
15 | Vapid
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/Internals/ECDHAgreement.cs:
--------------------------------------------------------------------------------
1 | namespace Lib.Net.Http.WebPush.Internals
2 | {
3 | internal readonly struct ECDHAgreement
4 | {
5 | public byte[] PublicKey { get; }
6 |
7 | public byte[] SharedSecretHmac { get; }
8 |
9 | public ECDHAgreement(byte[] publicKey, byte[] sharedSecretHmac)
10 | {
11 | PublicKey = publicKey;
12 | SharedSecretHmac = sharedSecretHmac;
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/Internals/ECDHAgreementCalculator.BouncyCastle.cs:
--------------------------------------------------------------------------------
1 | #if NET451 || NET461 || NETSTANDARD2_0
2 | using System;
3 | using System.IO;
4 | using System.Security.Cryptography;
5 | using Org.BouncyCastle.Asn1;
6 | using Org.BouncyCastle.Crypto;
7 | using Org.BouncyCastle.Asn1.X9;
8 | using Org.BouncyCastle.OpenSsl;
9 | using Org.BouncyCastle.Security;
10 | using Org.BouncyCastle.Asn1.Nist;
11 | using Org.BouncyCastle.Crypto.Parameters;
12 |
13 | namespace Lib.Net.Http.WebPush.Internals
14 | {
15 | internal static class ECDHAgreementCalculator
16 | {
17 | private const string PRIVATE_DER_IDENTIFIER = "1.2.840.10045.3.1.7";
18 | private const string PUBLIC_DER_IDENTIFIER = "1.2.840.10045.2.1";
19 |
20 | private const string PUBLIC_PEM_KEY_PREFIX = "-----BEGIN PUBLIC KEY-----\n";
21 | private const string PUBLIC_PEM_KEY_SUFFIX = "\n-----END PUBLIC KEY-----";
22 |
23 | private const string P256_CURVE_NAME = "P-256";
24 | private const string ECDH_ALGORITHM_NAME = "ECDH";
25 |
26 | public static ECDHAgreement CalculateAgreement(byte[] otherPartyPublicKey, byte[] hmacKey)
27 | {
28 | AsymmetricCipherKeyPair agreementKeyPair = GenerateAsymmetricCipherKeyPair();
29 | byte[] agreementPublicKey = ((ECPublicKeyParameters)agreementKeyPair.Public).Q.GetEncoded(false);
30 |
31 | IBasicAgreement agreement = AgreementUtilities.GetBasicAgreement(ECDH_ALGORITHM_NAME);
32 | agreement.Init(agreementKeyPair.Private);
33 |
34 | byte[] sharedSecret = agreement.CalculateAgreement(GetECPublicKeyParameters(otherPartyPublicKey)).ToByteArrayUnsigned();
35 |
36 | return new ECDHAgreement(agreementPublicKey, HmacSha256(hmacKey, sharedSecret));
37 | }
38 |
39 | private static AsymmetricCipherKeyPair GenerateAsymmetricCipherKeyPair()
40 | {
41 | X9ECParameters ecParameters = NistNamedCurves.GetByName(P256_CURVE_NAME);
42 | ECDomainParameters ecDomainParameters = new ECDomainParameters(ecParameters.Curve, ecParameters.G, ecParameters.N, ecParameters.H, ecParameters.GetSeed());
43 |
44 | IAsymmetricCipherKeyPairGenerator keyPairGenerator = GeneratorUtilities.GetKeyPairGenerator(ECDH_ALGORITHM_NAME);
45 | keyPairGenerator.Init(new ECKeyGenerationParameters(ecDomainParameters, new SecureRandom()));
46 |
47 | return keyPairGenerator.GenerateKeyPair();
48 | }
49 |
50 | private static ECPublicKeyParameters GetECPublicKeyParameters(byte[] publicKey)
51 | {
52 | Asn1Object derSequence = new DerSequence(
53 | new DerSequence(new DerObjectIdentifier(PUBLIC_DER_IDENTIFIER), new DerObjectIdentifier(PRIVATE_DER_IDENTIFIER)),
54 | new DerBitString(publicKey)
55 | );
56 |
57 | string pemKey = PUBLIC_PEM_KEY_PREFIX
58 | + Convert.ToBase64String(derSequence.GetDerEncoded())
59 | + PUBLIC_PEM_KEY_SUFFIX;
60 |
61 | PemReader pemKeyReader = new PemReader(new StringReader(pemKey));
62 | return (ECPublicKeyParameters)pemKeyReader.ReadObject();
63 | }
64 |
65 | private static byte[] HmacSha256(byte[] key, byte[] value)
66 | {
67 | byte[] hash = null;
68 |
69 | using (HMACSHA256 hasher = new HMACSHA256(key))
70 | {
71 | hash = hasher.ComputeHash(value);
72 | }
73 |
74 | return hash;
75 | }
76 | }
77 | }
78 | #endif
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/Internals/ECDHAgreementCalculator.NET.cs:
--------------------------------------------------------------------------------
1 | #if !NET451 && !NET461 && !NETSTANDARD2_0
2 | using System;
3 | using System.Formats.Asn1;
4 | using System.Security.Cryptography;
5 |
6 | namespace Lib.Net.Http.WebPush.Internals
7 | {
8 | internal static class ECDHAgreementCalculator
9 | {
10 | private const string PRIVATE_DER_IDENTIFIER = "1.2.840.10045.3.1.7";
11 | private const string PUBLIC_DER_IDENTIFIER = "1.2.840.10045.2.1";
12 |
13 | private const string PUBLIC_PEM_KEY_PREFIX = "-----BEGIN PUBLIC KEY-----";
14 | private const string PUBLIC_PEM_KEY_SUFFIX = "-----END PUBLIC KEY-----";
15 |
16 | public static ECDHAgreement CalculateAgreement(byte[] otherPartyPublicKey, byte[] hmacKey)
17 | {
18 | using (ECDiffieHellman agreement = ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256))
19 | {
20 | byte[] agreementPublicKey = GetAgreementPublicKey(agreement);
21 |
22 | byte[] sharedSecretHmac = agreement.DeriveKeyFromHmac(GetECDiffieHellmanPublicKey(otherPartyPublicKey), HashAlgorithmName.SHA256, hmacKey);
23 |
24 | return new ECDHAgreement(agreementPublicKey, sharedSecretHmac);
25 | }
26 | }
27 |
28 | private static byte[] GetAgreementPublicKey(ECDiffieHellman agreement)
29 | {
30 | ECParameters agreementParameters = agreement.ExportParameters(false);
31 |
32 | byte[] agreementPublicKey = new byte[agreementParameters.Q.X.Length + agreementParameters.Q.Y.Length + 1];
33 |
34 | agreementPublicKey[0] = 0x04;
35 | Array.Copy(agreementParameters.Q.X, 0, agreementPublicKey, 1, agreementParameters.Q.X.Length);
36 | Array.Copy(agreementParameters.Q.Y, 0, agreementPublicKey, agreementParameters.Q.X.Length + 1, agreementParameters.Q.Y.Length);
37 |
38 | return agreementPublicKey;
39 | }
40 |
41 | private static ECDiffieHellmanPublicKey GetECDiffieHellmanPublicKey(byte[] publicKey)
42 | {
43 | using (ECDiffieHellman ecdhAgreement = ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256))
44 | {
45 | ecdhAgreement.ImportFromPem(GetPublicKeyPem(publicKey));
46 |
47 | return ecdhAgreement.PublicKey;
48 | }
49 | }
50 |
51 | private static ReadOnlySpan GetPublicKeyPem(byte[] publicKey)
52 | {
53 | AsnWriter asnWriter = new AsnWriter(AsnEncodingRules.DER);
54 | asnWriter.PushSequence();
55 | asnWriter.PushSequence();
56 | asnWriter.WriteObjectIdentifier(PUBLIC_DER_IDENTIFIER);
57 | asnWriter.WriteObjectIdentifier(PRIVATE_DER_IDENTIFIER);
58 | asnWriter.PopSequence();
59 | asnWriter.WriteBitString(publicKey);
60 | asnWriter.PopSequence();
61 |
62 | return PUBLIC_PEM_KEY_PREFIX + Environment.NewLine
63 | + Convert.ToBase64String(asnWriter.Encode()) + Environment.NewLine
64 | + PUBLIC_PEM_KEY_SUFFIX;
65 | }
66 | }
67 | }
68 | #endif
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/Internals/ES256Signer.BouncyCastle.cs:
--------------------------------------------------------------------------------
1 | #if NET451 || NET461 || NETSTANDARD2_0
2 | using System;
3 | using System.IO;
4 | using System.Text;
5 | using System.Security.Cryptography;
6 | using Org.BouncyCastle.Asn1;
7 | using Org.BouncyCastle.Math;
8 | using Org.BouncyCastle.Crypto;
9 | using Org.BouncyCastle.OpenSsl;
10 | using Org.BouncyCastle.Crypto.Signers;
11 | using Org.BouncyCastle.Crypto.Parameters;
12 |
13 | namespace Lib.Net.Http.WebPush.Internals
14 | {
15 | internal class ES256Signer : IDisposable
16 | {
17 | private const string PRIVATE_DER_IDENTIFIER = "1.2.840.10045.3.1.7";
18 | private const string PRIVATE_PEM_KEY_PREFIX = "-----BEGIN EC PRIVATE KEY-----\n";
19 | private const string PRIVATE_PEM_KEY_SUFFIX = "\n-----END EC PRIVATE KEY----";
20 |
21 | private readonly ECDsaSigner _internalSigner;
22 |
23 | public ES256Signer(byte[] privateKey)
24 | {
25 | _internalSigner = new ECDsaSigner();
26 | _internalSigner.Init(true, GetECPrivateKeyParameters(privateKey));
27 | }
28 |
29 | public byte[] GenerateSignature(string input)
30 | {
31 | byte[] hash = ComputeHash(input);
32 |
33 | BigInteger[] signature = _internalSigner.GenerateSignature(hash);
34 |
35 | byte[] jwtSignatureFirstSegment = signature[0].ToByteArrayUnsigned();
36 | byte[] jwtSignatureSecondSegment = signature[1].ToByteArrayUnsigned();
37 |
38 | int jwtSignatureSegmentLength = Math.Max(jwtSignatureFirstSegment.Length, jwtSignatureSecondSegment.Length);
39 | byte[] combinedJwtSignature = new byte[2 * jwtSignatureSegmentLength];
40 | ByteArrayCopyWithPadLeft(jwtSignatureFirstSegment, combinedJwtSignature, 0, jwtSignatureSegmentLength);
41 | ByteArrayCopyWithPadLeft(jwtSignatureSecondSegment, combinedJwtSignature, jwtSignatureSegmentLength, jwtSignatureSegmentLength);
42 |
43 |
44 | return combinedJwtSignature;
45 | }
46 |
47 | public void Dispose()
48 | { }
49 |
50 | private static byte[] ComputeHash(string input)
51 | {
52 | using (var sha256Hasher = SHA256.Create())
53 | {
54 | return sha256Hasher.ComputeHash(Encoding.UTF8.GetBytes(input));
55 | }
56 | }
57 |
58 | private static ECPrivateKeyParameters GetECPrivateKeyParameters(byte[] privateKey)
59 | {
60 | Asn1Object derSequence = new DerSequence(
61 | new DerInteger(1),
62 | new DerOctetString(privateKey),
63 | new DerTaggedObject(0, new DerObjectIdentifier(PRIVATE_DER_IDENTIFIER))
64 | );
65 |
66 | string pemKey = PRIVATE_PEM_KEY_PREFIX
67 | + Convert.ToBase64String(derSequence.GetDerEncoded())
68 | + PRIVATE_PEM_KEY_SUFFIX;
69 |
70 | PemReader pemKeyReader = new PemReader(new StringReader(pemKey));
71 | AsymmetricCipherKeyPair keyPair = (AsymmetricCipherKeyPair)pemKeyReader.ReadObject();
72 |
73 | return (ECPrivateKeyParameters)keyPair.Private;
74 | }
75 |
76 | private static void ByteArrayCopyWithPadLeft(byte[] sourceArray, byte[] destinationArray, int destinationIndex, int destinationLengthToUse)
77 | {
78 | if (sourceArray.Length != destinationLengthToUse)
79 | {
80 | destinationIndex += (destinationLengthToUse - sourceArray.Length);
81 | }
82 |
83 | Buffer.BlockCopy(sourceArray, 0, destinationArray, destinationIndex, sourceArray.Length);
84 | }
85 | }
86 | }
87 | #endif
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/Internals/ES256Signer.NET.cs:
--------------------------------------------------------------------------------
1 | #if !NET451 && !NET461 && !NETSTANDARD2_0
2 | using System;
3 | using System.Text;
4 | using System.Formats.Asn1;
5 | using System.Security.Cryptography;
6 |
7 | namespace Lib.Net.Http.WebPush.Internals
8 | {
9 | internal class ES256Signer : IDisposable
10 | {
11 | private const string PRIVATE_DER_IDENTIFIER = "1.2.840.10045.3.1.7";
12 | private const string PRIVATE_PEM_KEY_PREFIX = "-----BEGIN EC PRIVATE KEY-----";
13 | private const string PRIVATE_PEM_KEY_SUFFIX = "-----END EC PRIVATE KEY-----";
14 |
15 | private readonly ECDsa _internalSigner;
16 |
17 | public ES256Signer(byte[] privateKey)
18 | {
19 | _internalSigner = ECDsa.Create(ECCurve.NamedCurves.nistP256);
20 | _internalSigner.ImportFromPem(GetPrivateKeyPem(privateKey));
21 | }
22 |
23 | public byte[] GenerateSignature(string input)
24 | {
25 | return _internalSigner.SignData(Encoding.UTF8.GetBytes(input), HashAlgorithmName.SHA256);
26 | }
27 |
28 | public void Dispose()
29 | {
30 | _internalSigner?.Dispose();
31 | }
32 |
33 | private static ReadOnlySpan GetPrivateKeyPem(byte[] privateKey)
34 | {
35 | AsnWriter asnWriter = new AsnWriter(AsnEncodingRules.DER);
36 | asnWriter.PushSequence();
37 | asnWriter.WriteInteger(1);
38 | asnWriter.WriteOctetString(privateKey);
39 | asnWriter.PushSetOf(new Asn1Tag(TagClass.ContextSpecific, 0, true));
40 | asnWriter.WriteObjectIdentifier(PRIVATE_DER_IDENTIFIER);
41 | asnWriter.PopSetOf(new Asn1Tag(TagClass.ContextSpecific, 0, true));
42 | asnWriter.PopSequence();
43 |
44 | return PRIVATE_PEM_KEY_PREFIX + Environment.NewLine
45 | + Convert.ToBase64String(asnWriter.Encode()) + Environment.NewLine
46 | + PRIVATE_PEM_KEY_SUFFIX;
47 | }
48 | }
49 | }
50 | #endif
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/Internals/UrlBase64Converter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Lib.Net.Http.WebPush.Internals
4 | {
5 | internal static class UrlBase64Converter
6 | {
7 | internal static byte[] FromUrlBase64String(string input)
8 | {
9 | input = input.Replace('-', '+').Replace('_', '/');
10 |
11 | while (input.Length % 4 != 0)
12 | {
13 | input += "=";
14 | }
15 |
16 | return Convert.FromBase64String(input);
17 | }
18 |
19 | internal static string ToUrlBase64String(byte[] input)
20 | {
21 | return Convert.ToBase64String(input).Replace('+', '-').Replace('/', '_').TrimEnd('=');
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/Lib.Net.Http.WebPush.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Lib.Net.Http.WebPush is a library which provides a Web Push Protocol based client for Push Service.
4 | Copyright © 2018 - 2025 Tomasz Pęczek
5 | 3.3.1
6 | Tomasz Pęczek
7 | net451;net461;netstandard2.0;net5.0;net6.0
8 | Lib.Net.Http.WebPush
9 | Lib.Net.Http.WebPush
10 | Lib.Net.Http.WebPush
11 | push;notifications;webpush;vapid
12 | https://github.com/tpeczek/Lib.Net.Http.WebPush
13 | MIT
14 | README.md
15 | git
16 | git://github.com/tpeczek/Lib.Net.Http.WebPush
17 | true
18 | true
19 | true
20 | true
21 | true
22 | true
23 | latest
24 | true
25 | Lib.Net.Http.WebPush.snk
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/Lib.Net.Http.WebPush.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpeczek/Lib.Net.Http.WebPush/2d84bc3ef915223491b5417c205be596064ec4a8/src/Lib.Net.Http.WebPush/Lib.Net.Http.WebPush.snk
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/PushEncryptionKeyName.cs:
--------------------------------------------------------------------------------
1 | namespace Lib.Net.Http.WebPush
2 | {
3 | ///
4 | /// The client keys shared as part of subscription.
5 | ///
6 | public enum PushEncryptionKeyName
7 | {
8 | ///
9 | /// The client P-256 public key for use in ECDH.
10 | ///
11 | P256DH,
12 | ///
13 | /// The client authentication secret.
14 | ///
15 | Auth
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/PushMessage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using System.Net.Http;
4 |
5 | namespace Lib.Net.Http.WebPush
6 | {
7 | ///
8 | /// Class representing a push message.
9 | ///
10 | public class PushMessage
11 | {
12 | #region Fields
13 | private static readonly string NOT_INSTANTIATED_THROUGH_STRING_BASED_CONSTRUCTOR = $"The {nameof(PushMessage)} instance hasn't been instantianted through string based constructor.";
14 |
15 | private readonly bool _stringBased;
16 |
17 | private string _content = null;
18 | private readonly HttpContent _httpContent = null;
19 | private int? _timeToLive;
20 | #endregion
21 |
22 | #region Properties
23 | ///
24 | /// Gets or sets the topic (used to correlate messages sent to the same subscription).
25 | ///
26 | public string Topic { get; set; }
27 |
28 | ///
29 | /// Gets or sets the content when a instance has been instantiated through .
30 | ///
31 | public string Content
32 | {
33 | get
34 | {
35 | if (!_stringBased)
36 | {
37 | throw new InvalidOperationException(NOT_INSTANTIATED_THROUGH_STRING_BASED_CONSTRUCTOR);
38 | }
39 |
40 | return _content;
41 | }
42 |
43 | set
44 | {
45 | if (!_stringBased)
46 | {
47 | throw new InvalidOperationException(NOT_INSTANTIATED_THROUGH_STRING_BASED_CONSTRUCTOR);
48 | }
49 |
50 | _content = value;
51 | }
52 | }
53 |
54 | ///
55 | /// Gets or sets the content.
56 | ///
57 | public HttpContent HttpContent
58 | {
59 | get
60 | {
61 | if (_stringBased)
62 | {
63 | return (_content is null) ? null : new StringContent(_content, Encoding.UTF8);
64 | }
65 |
66 | return _httpContent;
67 | }
68 | }
69 |
70 | ///
71 | /// Gets or sets the time (in seconds) for which the message should be retained by push service.
72 | ///
73 | public int? TimeToLive
74 | {
75 | get { return _timeToLive; }
76 |
77 | set
78 | {
79 | if (value.HasValue && (value.Value < 0))
80 | {
81 | throw new ArgumentOutOfRangeException(nameof(TimeToLive), "The TTL must be a non-negative integer");
82 | }
83 |
84 | _timeToLive = value;
85 | }
86 | }
87 |
88 | ///
89 | /// Gets or sets the message urgency.
90 | ///
91 | public PushMessageUrgency Urgency { get; set; }
92 | #endregion
93 |
94 | #region Constructors
95 | ///
96 | /// Creates new instance of class.
97 | ///
98 | /// The content.
99 | public PushMessage(string content)
100 | {
101 | _stringBased = true;
102 | Content = content;
103 | Urgency = PushMessageUrgency.Normal;
104 | }
105 |
106 | ///
107 | /// Creates new instance of class.
108 | ///
109 | /// The content.
110 | public PushMessage(HttpContent content)
111 | {
112 | _stringBased = false;
113 | _httpContent = content;
114 | Urgency = PushMessageUrgency.Normal;
115 | }
116 | #endregion
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/PushMessageUrgency.cs:
--------------------------------------------------------------------------------
1 | namespace Lib.Net.Http.WebPush
2 | {
3 | ///
4 | /// The push message urgency.
5 | ///
6 | public enum PushMessageUrgency
7 | {
8 | ///
9 | /// Very low (e.g. advertisements).
10 | ///
11 | VeryLow,
12 | ///
13 | /// Low (e.g. topic updates).
14 | ///
15 | Low,
16 | ///
17 | /// Normal (e.g. chat message).
18 | ///
19 | Normal,
20 | ///
21 | /// High (e.g. time-sensitive alert).
22 | ///
23 | High
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/PushServiceClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Net.Http;
4 | using System.Net.Http.Headers;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Globalization;
8 | using System.Threading.Tasks;
9 | using System.Collections.Generic;
10 | using System.Security.Cryptography;
11 | using Lib.Net.Http.EncryptedContentEncoding;
12 | using Lib.Net.Http.WebPush.Internals;
13 | using Lib.Net.Http.WebPush.Authentication;
14 |
15 | namespace Lib.Net.Http.WebPush
16 | {
17 | ///
18 | /// A Web Push Protocol compliant client for push service.
19 | ///
20 | ///
21 | /// The should be considered an expensive object as it internally holds an instance of class. In order to avoid Improper Instantiation antipattern a shared singleton instance should be created or a pool of reusable instances should be used.
22 | ///
23 | public class PushServiceClient
24 | {
25 | #region Fields
26 | private const string TTL_HEADER_NAME = "TTL";
27 | private const string TOPIC_HEADER_NAME = "Topic";
28 | private const string URGENCY_HEADER_NAME = "Urgency";
29 | private const string CRYPTO_KEY_HEADER_NAME = "Crypto-Key";
30 | private const string WEBPUSH_AUTHENTICATION_SCHEME = "WebPush";
31 | private const string VAPID_AUTHENTICATION_SCHEME = "vapid";
32 |
33 | private const int DEFAULT_TIME_TO_LIVE = 2419200;
34 |
35 | private const string KEYING_MATERIAL_INFO_PARAMETER_PREFIX = "WebPush: info";
36 | private const byte KEYING_MATERIAL_INFO_PARAMETER_DELIMITER = 1;
37 |
38 | private const int CONTENT_RECORD_SIZE = 4096;
39 |
40 | private static readonly byte[] _keyingMaterialInfoParameterPrefix = Encoding.ASCII.GetBytes(KEYING_MATERIAL_INFO_PARAMETER_PREFIX);
41 | private static readonly Dictionary _urgencyHeaderValues = new Dictionary
42 | {
43 | { PushMessageUrgency.VeryLow, "very-low" },
44 | { PushMessageUrgency.Low, "low" },
45 | { PushMessageUrgency.High, "high" }
46 | };
47 |
48 | private int _defaultTimeToLive = DEFAULT_TIME_TO_LIVE;
49 |
50 | private readonly HttpClient _httpClient;
51 | #endregion
52 |
53 | #region Properties
54 | ///
55 | /// Gets or sets the value indicating if client should automatically attempt to retry in case of 429 Too Many Requests.
56 | ///
57 | public bool AutoRetryAfter { get; set; } = true;
58 |
59 | ///
60 | /// Gets or sets the value indicating the maximum number of automatic attempts to retry in case of 429 Too Many Requests (<= 0 means unlimited).
61 | ///
62 | public int MaxRetriesAfter { get; set; } = 0;
63 |
64 | ///
65 | /// Gets or sets the default time (in seconds) for which the message should be retained by push service. It will be used when is not set.
66 | ///
67 | public int DefaultTimeToLive
68 | {
69 | get { return _defaultTimeToLive; }
70 |
71 | set
72 | {
73 | if (value < 0)
74 | {
75 | throw new ArgumentOutOfRangeException(nameof(DefaultTimeToLive), "The TTL must be a non-negative integer");
76 | }
77 |
78 | _defaultTimeToLive = value;
79 | }
80 | }
81 |
82 | ///
83 | /// Gets or sets the default authentication details.
84 | ///
85 | public VapidAuthentication DefaultAuthentication { get; set; }
86 |
87 | ///
88 | /// Gets or sets the default to be used.
89 | ///
90 | public VapidAuthenticationScheme DefaultAuthenticationScheme { get; set; } = VapidAuthenticationScheme.Vapid;
91 | #endregion
92 |
93 | #region Constructors
94 | ///
95 | /// Creates new instance of class.
96 | ///
97 | public PushServiceClient()
98 | : this(new HttpClient())
99 | { }
100 |
101 | ///
102 | /// Creates new instance of class.
103 | ///
104 | /// The HttpClient instance.
105 | public PushServiceClient(HttpClient httpClient)
106 | {
107 | _httpClient = httpClient;
108 | }
109 | #endregion
110 |
111 | #region Methods
112 | ///
113 | /// Requests delivery of push message by push service as an asynchronous operation.
114 | ///
115 | /// The push service subscription.
116 | /// The push message.
117 | /// The task object representing the asynchronous operation.
118 | public Task RequestPushMessageDeliveryAsync(PushSubscription subscription, PushMessage message)
119 | {
120 | return RequestPushMessageDeliveryAsync(subscription, message, null, DefaultAuthenticationScheme, CancellationToken.None);
121 | }
122 |
123 | ///
124 | /// Requests delivery of push message by push service as an asynchronous operation.
125 | ///
126 | /// The push service subscription.
127 | /// The push message.
128 | /// The cancellation token to cancel operation.
129 | /// The task object representing the asynchronous operation.
130 | public Task RequestPushMessageDeliveryAsync(PushSubscription subscription, PushMessage message, CancellationToken cancellationToken)
131 | {
132 | return RequestPushMessageDeliveryAsync(subscription, message, null, DefaultAuthenticationScheme, cancellationToken);
133 | }
134 |
135 | ///
136 | /// Requests delivery of push message by push service as an asynchronous operation.
137 | ///
138 | /// The push service subscription.
139 | /// The push message.
140 | /// The authentication details.
141 | /// The task object representing the asynchronous operation.
142 | public Task RequestPushMessageDeliveryAsync(PushSubscription subscription, PushMessage message, VapidAuthentication authentication)
143 | {
144 | return RequestPushMessageDeliveryAsync(subscription, message, authentication, DefaultAuthenticationScheme, CancellationToken.None);
145 | }
146 |
147 | ///
148 | /// Requests delivery of push message by push service as an asynchronous operation.
149 | ///
150 | /// The push service subscription.
151 | /// The push message.
152 | /// The authentication details.
153 | /// The to use.
154 | /// The task object representing the asynchronous operation.
155 | public Task RequestPushMessageDeliveryAsync(PushSubscription subscription, PushMessage message, VapidAuthentication authentication, VapidAuthenticationScheme authenticationScheme)
156 | {
157 | return RequestPushMessageDeliveryAsync(subscription, message, authentication, authenticationScheme, CancellationToken.None);
158 | }
159 |
160 | ///
161 | /// Requests delivery of push message by push service as an asynchronous operation.
162 | ///
163 | /// The push service subscription.
164 | /// The push message.
165 | /// The authentication details.
166 | /// The cancellation token to cancel operation.
167 | /// The task object representing the asynchronous operation.
168 | public Task RequestPushMessageDeliveryAsync(PushSubscription subscription, PushMessage message, VapidAuthentication authentication, CancellationToken cancellationToken)
169 | {
170 | return RequestPushMessageDeliveryAsync(subscription, message, authentication, DefaultAuthenticationScheme, cancellationToken);
171 | }
172 |
173 | ///
174 | /// Requests delivery of push message by push service as an asynchronous operation.
175 | ///
176 | /// The push service subscription.
177 | /// The push message.
178 | /// The authentication details.
179 | /// The to use.
180 | /// The cancellation token to cancel operation.
181 | /// The task object representing the asynchronous operation.
182 | public async Task RequestPushMessageDeliveryAsync(PushSubscription subscription, PushMessage message, VapidAuthentication authentication, VapidAuthenticationScheme authenticationScheme, CancellationToken cancellationToken)
183 | {
184 | HttpRequestMessage pushMessageDeliveryRequest = PreparePushMessageDeliveryRequest(subscription, message, authentication, authenticationScheme);
185 | HttpResponseMessage pushMessageDeliveryRequestResponse = null;
186 |
187 | try
188 | {
189 | pushMessageDeliveryRequestResponse = await _httpClient.SendAsync(pushMessageDeliveryRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
190 |
191 | int retriesAfterCount = 0;
192 | while (ShouldRetryAfter(pushMessageDeliveryRequestResponse, retriesAfterCount, out TimeSpan delay))
193 | {
194 | pushMessageDeliveryRequest.Dispose();
195 | pushMessageDeliveryRequestResponse.Dispose();
196 |
197 | await Task.Delay(delay, cancellationToken);
198 |
199 | pushMessageDeliveryRequest = PreparePushMessageDeliveryRequest(subscription, message, authentication, authenticationScheme);
200 | pushMessageDeliveryRequestResponse = await _httpClient.SendAsync(pushMessageDeliveryRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
201 |
202 | retriesAfterCount++;
203 | }
204 |
205 | await HandlePushMessageDeliveryRequestResponse(pushMessageDeliveryRequestResponse, subscription);
206 | }
207 | finally
208 | {
209 | pushMessageDeliveryRequest.Dispose();
210 | pushMessageDeliveryRequestResponse?.Dispose();
211 | }
212 | }
213 |
214 | private HttpRequestMessage PreparePushMessageDeliveryRequest(PushSubscription subscription, PushMessage message, VapidAuthentication authentication, VapidAuthenticationScheme authenticationScheme)
215 | {
216 | authentication = authentication ?? DefaultAuthentication;
217 | if (authentication == null)
218 | {
219 | throw new InvalidOperationException("The VAPID authentication information is not available");
220 | }
221 |
222 | HttpRequestMessage pushMessageDeliveryRequest = new HttpRequestMessage(HttpMethod.Post, subscription.Endpoint)
223 | {
224 | Headers =
225 | {
226 | { TTL_HEADER_NAME, (message.TimeToLive ?? DefaultTimeToLive).ToString(CultureInfo.InvariantCulture) }
227 | }
228 | };
229 | pushMessageDeliveryRequest = SetAuthentication(pushMessageDeliveryRequest, subscription, authentication, authenticationScheme);
230 | pushMessageDeliveryRequest = SetUrgency(pushMessageDeliveryRequest, message);
231 | pushMessageDeliveryRequest = SetTopic(pushMessageDeliveryRequest, message);
232 | pushMessageDeliveryRequest = SetContent(pushMessageDeliveryRequest, subscription, message);
233 |
234 | return pushMessageDeliveryRequest;
235 | }
236 |
237 | private static HttpRequestMessage SetAuthentication(HttpRequestMessage pushMessageDeliveryRequest, PushSubscription subscription, VapidAuthentication authentication, VapidAuthenticationScheme authenticationScheme)
238 | {
239 | Uri endpointUri = new Uri(subscription.Endpoint);
240 | string audience = endpointUri.Scheme + @"://" + endpointUri.Host;
241 |
242 | if (authenticationScheme == VapidAuthenticationScheme.WebPush)
243 | {
244 | VapidAuthentication.WebPushSchemeHeadersValues webPushSchemeHeadersValues = authentication.GetWebPushSchemeHeadersValues(audience);
245 |
246 | pushMessageDeliveryRequest.Headers.Authorization = new AuthenticationHeaderValue(WEBPUSH_AUTHENTICATION_SCHEME, webPushSchemeHeadersValues.AuthenticationHeaderValueParameter);
247 | pushMessageDeliveryRequest.Headers.Add(CRYPTO_KEY_HEADER_NAME, webPushSchemeHeadersValues.CryptoKeyHeaderValue);
248 | }
249 | else
250 | {
251 | pushMessageDeliveryRequest.Headers.Authorization = new AuthenticationHeaderValue(VAPID_AUTHENTICATION_SCHEME, authentication.GetVapidSchemeAuthenticationHeaderValueParameter(audience));
252 | }
253 |
254 | return pushMessageDeliveryRequest;
255 | }
256 |
257 | private static HttpRequestMessage SetUrgency(HttpRequestMessage pushMessageDeliveryRequest, PushMessage message)
258 | {
259 | switch (message.Urgency)
260 | {
261 | case PushMessageUrgency.Normal:
262 | break;
263 | case PushMessageUrgency.VeryLow:
264 | case PushMessageUrgency.Low:
265 | case PushMessageUrgency.High:
266 | pushMessageDeliveryRequest.Headers.Add(URGENCY_HEADER_NAME, _urgencyHeaderValues[message.Urgency]);
267 | break;
268 | default:
269 | throw new NotSupportedException($"Not supported value has been provided for {nameof(PushMessageUrgency)}.");
270 | }
271 |
272 | return pushMessageDeliveryRequest;
273 | }
274 |
275 | private static HttpRequestMessage SetTopic(HttpRequestMessage pushMessageDeliveryRequest, PushMessage message)
276 | {
277 | if (!String.IsNullOrWhiteSpace(message.Topic))
278 | {
279 | pushMessageDeliveryRequest.Headers.Add(TOPIC_HEADER_NAME, message.Topic);
280 | }
281 |
282 | return pushMessageDeliveryRequest;
283 | }
284 |
285 | private static HttpRequestMessage SetContent(HttpRequestMessage pushMessageDeliveryRequest, PushSubscription subscription, PushMessage message)
286 | {
287 | HttpContent httpContent = message.HttpContent;
288 | if (httpContent is null)
289 | {
290 | pushMessageDeliveryRequest.Content = null;
291 | }
292 | else
293 | {
294 | string p256dhKey = subscription.GetKey(PushEncryptionKeyName.P256DH);
295 | string authKey = subscription.GetKey(PushEncryptionKeyName.Auth);
296 |
297 | if (String.IsNullOrWhiteSpace(p256dhKey) || String.IsNullOrWhiteSpace(authKey))
298 | {
299 | throw new InvalidOperationException("The client P-256 public key or authentication secret is not available");
300 | }
301 |
302 | ECDHAgreement keyAgreement = ECDHAgreementCalculator.CalculateAgreement
303 | (
304 | UrlBase64Converter.FromUrlBase64String(p256dhKey),
305 | UrlBase64Converter.FromUrlBase64String(authKey)
306 | );
307 |
308 | pushMessageDeliveryRequest.Content = new Aes128GcmEncodedContent(
309 | httpContent,
310 | GetKeyingMaterial(subscription, keyAgreement),
311 | keyAgreement.PublicKey,
312 | CONTENT_RECORD_SIZE
313 | );
314 | }
315 |
316 | return pushMessageDeliveryRequest;
317 | }
318 |
319 | private static byte[] GetKeyingMaterial(PushSubscription subscription, ECDHAgreement keyAgreement)
320 | {
321 | byte[] userAgentPublicKey = UrlBase64Converter.FromUrlBase64String(subscription.GetKey(PushEncryptionKeyName.P256DH));
322 |
323 | byte[] infoParameter = GetKeyingMaterialInfoParameter(userAgentPublicKey, keyAgreement.PublicKey);
324 | byte[] keyingMaterial = HmacSha256(keyAgreement.SharedSecretHmac, infoParameter);
325 |
326 | return keyingMaterial;
327 | }
328 |
329 | private static byte[] GetKeyingMaterialInfoParameter(byte[] userAgentPublicKey, byte[] applicationServerPublicKey)
330 | {
331 | // "WebPush: info" || 0x00 || ua_public || as_public || 0x01
332 | byte[] infoParameter = new byte[_keyingMaterialInfoParameterPrefix.Length + userAgentPublicKey.Length + applicationServerPublicKey.Length + 2];
333 |
334 | Buffer.BlockCopy(_keyingMaterialInfoParameterPrefix, 0, infoParameter, 0, _keyingMaterialInfoParameterPrefix.Length);
335 | int infoParameterIndex = _keyingMaterialInfoParameterPrefix.Length + 1;
336 |
337 | Buffer.BlockCopy(userAgentPublicKey, 0, infoParameter, infoParameterIndex, userAgentPublicKey.Length);
338 | infoParameterIndex += userAgentPublicKey.Length;
339 |
340 | Buffer.BlockCopy(applicationServerPublicKey, 0, infoParameter, infoParameterIndex, applicationServerPublicKey.Length);
341 | infoParameter[infoParameter.Length - 1] = KEYING_MATERIAL_INFO_PARAMETER_DELIMITER;
342 |
343 | return infoParameter;
344 | }
345 |
346 | private static byte[] HmacSha256(byte[] key, byte[] value)
347 | {
348 | byte[] hash = null;
349 |
350 | using (HMACSHA256 hasher = new HMACSHA256(key))
351 | {
352 | hash = hasher.ComputeHash(value);
353 | }
354 |
355 | return hash;
356 | }
357 |
358 | private bool ShouldRetryAfter(HttpResponseMessage pushMessageDeliveryRequestResponse, int retriesAfterCount, out TimeSpan delay)
359 | {
360 | delay = TimeSpan.MinValue;
361 |
362 | if ((pushMessageDeliveryRequestResponse.StatusCode != (HttpStatusCode)429) || !AutoRetryAfter)
363 | {
364 | return false;
365 | }
366 |
367 | if ((MaxRetriesAfter > 0) && (retriesAfterCount >= MaxRetriesAfter))
368 | {
369 | return false;
370 | }
371 |
372 | if ((pushMessageDeliveryRequestResponse.Headers.RetryAfter is null) || (!pushMessageDeliveryRequestResponse.Headers.RetryAfter.Date.HasValue && !pushMessageDeliveryRequestResponse.Headers.RetryAfter.Delta.HasValue))
373 | {
374 | return false;
375 | }
376 |
377 | if (pushMessageDeliveryRequestResponse.Headers.RetryAfter.Delta.HasValue)
378 | {
379 | delay = pushMessageDeliveryRequestResponse.Headers.RetryAfter.Delta.Value;
380 | }
381 |
382 | if (pushMessageDeliveryRequestResponse.Headers.RetryAfter.Date.HasValue)
383 | {
384 | delay = pushMessageDeliveryRequestResponse.Headers.RetryAfter.Date.Value.Subtract(DateTimeOffset.UtcNow);
385 | }
386 |
387 | return true;
388 | }
389 |
390 | private static async Task HandlePushMessageDeliveryRequestResponse(HttpResponseMessage pushMessageDeliveryRequestResponse, PushSubscription subscription)
391 | {
392 | if (PushMessageDeliverySuccessful(pushMessageDeliveryRequestResponse.StatusCode))
393 | {
394 | return;
395 | }
396 |
397 | string reason = String.IsNullOrWhiteSpace(pushMessageDeliveryRequestResponse.ReasonPhrase) ?
398 | $"Received unexpected response code: {pushMessageDeliveryRequestResponse.StatusCode}"
399 | : pushMessageDeliveryRequestResponse.ReasonPhrase;
400 |
401 | string content = await pushMessageDeliveryRequestResponse.Content.ReadAsStringAsync();
402 |
403 | throw new PushServiceClientException(reason, pushMessageDeliveryRequestResponse.StatusCode, pushMessageDeliveryRequestResponse.Headers, content, subscription);
404 | }
405 |
406 | private static bool PushMessageDeliverySuccessful(HttpStatusCode statusCode)
407 | {
408 | switch (statusCode)
409 | {
410 | case HttpStatusCode.Created: // RFC 8030
411 | case HttpStatusCode.OK: // Mozilla Autopush Server (Delivered to node client is connected to)
412 | case HttpStatusCode.Accepted: // Mozilla Autopush Server (Stored for delivery to client at a later time)
413 | return true;
414 | default:
415 | return false;
416 | }
417 | }
418 | #endregion
419 | }
420 | }
421 |
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/PushServiceClientException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Net.Http.Headers;
4 |
5 | namespace Lib.Net.Http.WebPush
6 | {
7 | ///
8 | /// An exception representing requesting delivery failure based on push service response.
9 | ///
10 | public class PushServiceClientException : Exception
11 | {
12 | #region Properties
13 | ///
14 | /// Gets the status code of the push service response.
15 | ///
16 | public HttpStatusCode StatusCode { get; }
17 |
18 | ///
19 | /// Gets the headers of the push service response.
20 | ///
21 | public HttpResponseHeaders Headers { get; }
22 |
23 | ///
24 | /// Gets the body of the push service response.
25 | ///
26 | public string Body { get; }
27 |
28 | ///
29 | /// Gets the that initiated the push service request.
30 | ///
31 | public PushSubscription PushSubscription { get; }
32 | #endregion
33 |
34 | #region Constructors
35 | ///
36 | /// Creates new instance of class.
37 | ///
38 | /// The message that describes the current exception.
39 | /// The status code of the push service response.
40 | public PushServiceClientException(string message, HttpStatusCode statusCode)
41 | : this(message, statusCode, null, null, null)
42 | {
43 | }
44 |
45 | ///
46 | /// Creates new instance of class.
47 | ///
48 | /// The message that describes the current exception.
49 | /// The status code of the push service response.
50 | /// The headers of the push service response.
51 | /// The body of the push service response.
52 | /// The that initiated the push service request.
53 | public PushServiceClientException(string message, HttpStatusCode statusCode, HttpResponseHeaders headers, string body, PushSubscription pushSubscription)
54 | : base(message)
55 | {
56 | StatusCode = statusCode;
57 | Headers = headers;
58 | Body = body;
59 | PushSubscription = pushSubscription;
60 | }
61 | #endregion
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Lib.Net.Http.WebPush/PushSubscription.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Collections.Generic;
4 |
5 | namespace Lib.Net.Http.WebPush
6 | {
7 | ///
8 | /// Class representing a push subscription
9 | ///
10 | public class PushSubscription
11 | {
12 | #region Properties
13 | ///
14 | /// Gets or sets the subscription endpoint.
15 | ///
16 | public string Endpoint { get; set; }
17 |
18 | ///
19 | /// Gets or sets client keys shared as part of subscription.
20 | ///
21 | public IDictionary Keys { get; set; }
22 | #endregion
23 |
24 | #region Methods
25 | ///
26 | /// Gets specific client key shared as part of subscription.
27 | ///
28 | /// The key name.
29 | /// The key.
30 | public string GetKey(PushEncryptionKeyName keyName)
31 | {
32 | string key = null;
33 |
34 | if (Keys != null)
35 | {
36 | string keyNameStringified = StringifyKeyName(keyName);
37 |
38 | if (Keys.ContainsKey(keyNameStringified))
39 | {
40 | key = Keys[keyNameStringified];
41 | }
42 | else
43 | {
44 | key = Keys.SingleOrDefault(x => String.Equals(x.Key, keyNameStringified, StringComparison.OrdinalIgnoreCase)).Value;
45 | }
46 | }
47 |
48 | return key;
49 | }
50 |
51 | ///
52 | /// Sets specific client key shared as part of subscription.
53 | ///
54 | /// The key name.
55 | /// The key.
56 | public void SetKey(PushEncryptionKeyName keyName, string key)
57 | {
58 | if (Keys == null)
59 | {
60 | Keys = new Dictionary();
61 | }
62 |
63 | Keys[StringifyKeyName(keyName)] = key;
64 | }
65 |
66 | private string StringifyKeyName(PushEncryptionKeyName keyName)
67 | {
68 | return keyName.ToString().ToLowerInvariant();
69 | }
70 | #endregion
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/test/Test.Lib.Net.Http.WebPush/Functional/Infrastructure/FakePushServiceApplicationFactory.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using Microsoft.AspNetCore;
3 | using Microsoft.AspNetCore.Hosting;
4 | using Microsoft.AspNetCore.Mvc.Testing;
5 |
6 | namespace Test.Lib.Net.Http.WebPush.Functional.Infrastructure
7 | {
8 | public class FakePushServiceApplicationFactory : WebApplicationFactory
9 | {
10 | protected override IWebHostBuilder CreateWebHostBuilder()
11 | {
12 | return WebHost.CreateDefaultBuilder()
13 | .UseStartup();
14 | }
15 |
16 | protected override void ConfigureWebHost(IWebHostBuilder builder)
17 | {
18 | builder.UseContentRoot(Path.GetTempPath());
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/test/Test.Lib.Net.Http.WebPush/Functional/Infrastructure/FakePushServiceStartup.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Microsoft.AspNetCore.Http;
3 | using Microsoft.AspNetCore.Builder;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.AspNetCore.Http.Features;
6 |
7 | namespace Test.Lib.Net.Http.WebPush.Functional.Infrastructure
8 | {
9 | public class FakePushServiceStartup
10 | {
11 | #region Fields
12 | public const int OTHER_CLIENT_ERROR_STATUS_CODE = 499;
13 | public const string OTHER_CLIENT_ERROR_REASON_PHRASE = "Other Client Error";
14 | public const string OTHER_CLIENT_ERROR_BODY = "{\"code\": 499,\"errno\": 199,\"error\": \"Other Client Error\",\"message\": \"Some other client error occured\"}";
15 |
16 | private bool shouldRetryAfter = true;
17 | #endregion
18 |
19 | public void ConfigureServices(IServiceCollection services)
20 | { }
21 |
22 | public void Configure(IApplicationBuilder app)
23 | {
24 | app.UseRouting()
25 | .UseEndpoints(endpoints =>
26 | {
27 | endpoints.MapPost("/push-rfc-8030-created", context =>
28 | {
29 | context.Response.StatusCode = StatusCodes.Status201Created;
30 |
31 | return Task.CompletedTask;
32 | });
33 |
34 | endpoints.MapPost("/mozilla-autopush-delivered", context =>
35 | {
36 | context.Response.StatusCode = StatusCodes.Status200OK;
37 |
38 | return Task.CompletedTask;
39 | });
40 |
41 | endpoints.MapPost("/mozilla-autopush-stored", context =>
42 | {
43 | context.Response.StatusCode = StatusCodes.Status202Accepted;
44 |
45 | return Task.CompletedTask;
46 | });
47 |
48 | endpoints.MapPost("/push-retry-after-once", context =>
49 | {
50 | if (shouldRetryAfter)
51 | {
52 | context.Response.StatusCode = 429;
53 | context.Response.Headers.Add("Retry-After", "5");
54 |
55 | shouldRetryAfter = false;
56 | }
57 | else
58 | {
59 | context.Response.StatusCode = StatusCodes.Status201Created;
60 |
61 | shouldRetryAfter = true;
62 | }
63 |
64 | return Task.CompletedTask;
65 | });
66 |
67 | endpoints.MapPost("/push-retry-after-always", context =>
68 | {
69 | context.Response.StatusCode = 429;
70 | context.Response.Headers.Add("Retry-After", "5");
71 |
72 | return Task.CompletedTask;
73 | });
74 |
75 | endpoints.MapPost("/push-client-error", async context =>
76 | {
77 | context.Response.StatusCode = OTHER_CLIENT_ERROR_STATUS_CODE;
78 | context.Response.HttpContext.Features.Get().ReasonPhrase = OTHER_CLIENT_ERROR_REASON_PHRASE;
79 | await context.Response.WriteAsync(OTHER_CLIENT_ERROR_BODY);
80 | });
81 | });
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/test/Test.Lib.Net.Http.WebPush/Functional/PushMessageDeliveryTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using System.Collections.Generic;
4 | using Xunit;
5 | using Lib.Net.Http.WebPush;
6 | using Lib.Net.Http.WebPush.Authentication;
7 | using Test.Lib.Net.Http.WebPush.Functional.Infrastructure;
8 |
9 | namespace Test.Lib.Net.Http.WebPush.Functional
10 | {
11 | public class PushMessageDeliveryTests : IClassFixture
12 | {
13 | #region Fields
14 | private const string RFC_8030_CREATED_ENDPOINT = "http://localhost/push-rfc-8030-created";
15 | private const string MOZILLA_AUTOPUSH_DELIVERED_ENDPOINT = "http://localhost/mozilla-autopush-delivered";
16 | private const string MOZILLA_AUTOPUSH_STORED_ENDPOINT = "http://localhost/mozilla-autopush-stored";
17 | private const string RETRY_AFTER_ONCE_ENDPOINT = "http://localhost/push-retry-after-once";
18 | private const string RETRY_AFTER_ALWAYS_ENDPOINT = "http://localhost/push-retry-after-always";
19 | private const string CLIENT_ERROR_ENDPOINT = "http://localhost/push-client-error";
20 |
21 | private const string WALRUS_CONTENT = "I am the walrus";
22 |
23 | private const string PUSH_SUBSCRIPTION_AUTH_KEY = "n5mG_PyMSKALsjsU542E6g";
24 | private const string PUSH_SUBSCRIPTION_P256DH_KEY = "BDS52l6tfaf6ZEqhyDa0cScvCi4WXNYPIwmfas-7nKLIQex-DVKXB9gUxDExaZEOiwovl6LbWXZBZ9AT-GWT6eQ";
25 |
26 | private readonly PushSubscription _pushSubscription = new PushSubscription
27 | {
28 | Keys = new Dictionary
29 | {
30 | { "auth", PUSH_SUBSCRIPTION_AUTH_KEY },
31 | { "p256dh", PUSH_SUBSCRIPTION_P256DH_KEY }
32 | }
33 | };
34 |
35 | private readonly VapidAuthentication _vapidAuthentication = new VapidAuthentication("BK5sn4jfa0Jqo9MhV01oyzK2FaEHm0KqkSCuUkKr53-9cr-vBE1a9TiiBaWy7hy0eOUF1jhZnwcd3vof4wnwSw0", "AJ2ho7or-6D4StPktpTO3l1ErjHGyxb0jzt9Y8lj67g")
36 | {
37 | Subject = "https://localhost:8080/"
38 | };
39 |
40 | private readonly FakePushServiceApplicationFactory _pushServiceFactory;
41 | #endregion
42 |
43 | #region Constructor
44 | public PushMessageDeliveryTests(FakePushServiceApplicationFactory pushServiceFactory)
45 | {
46 | _pushServiceFactory = pushServiceFactory;
47 | }
48 | #endregion
49 |
50 | #region Prepare SUT
51 | private PushServiceClient PreparePushServiceClient()
52 | {
53 | return new PushServiceClient(_pushServiceFactory.CreateClient())
54 | {
55 | DefaultAuthentication = _vapidAuthentication
56 | };
57 | }
58 | #endregion
59 |
60 | #region Tests
61 | [Theory]
62 | [InlineData(RFC_8030_CREATED_ENDPOINT)]
63 | [InlineData(MOZILLA_AUTOPUSH_DELIVERED_ENDPOINT)]
64 | [InlineData(MOZILLA_AUTOPUSH_STORED_ENDPOINT)]
65 | public async Task PushService_NoError_DeliversPushMessage(string endpoint)
66 | {
67 | _pushSubscription.Endpoint = endpoint;
68 |
69 | PushMessage pushMessage = new PushMessage(WALRUS_CONTENT);
70 |
71 | PushServiceClient pushClient = PreparePushServiceClient();
72 |
73 | Exception pushMessageDeliveryException = await Record.ExceptionAsync(async () =>
74 | {
75 | await pushClient.RequestPushMessageDeliveryAsync(_pushSubscription, pushMessage);
76 | });
77 |
78 | Assert.Null(pushMessageDeliveryException);
79 | }
80 |
81 | [Fact]
82 | public async Task PushService_TooManyRequests_DeliversPushMessageWithRetryAfter()
83 | {
84 | _pushSubscription.Endpoint = RETRY_AFTER_ONCE_ENDPOINT;
85 |
86 | PushMessage pushMessage = new PushMessage(WALRUS_CONTENT);
87 |
88 | PushServiceClient pushClient = PreparePushServiceClient();
89 |
90 | Exception pushMessageDeliveryException = await Record.ExceptionAsync(async () =>
91 | {
92 | await pushClient.RequestPushMessageDeliveryAsync(_pushSubscription, pushMessage);
93 | });
94 |
95 | Assert.Null(pushMessageDeliveryException);
96 | }
97 |
98 | [Fact]
99 | public async Task PushService_TooManyRequests_MaxRetriesAfter_ThrowsPushServiceClientException()
100 | {
101 | _pushSubscription.Endpoint = RETRY_AFTER_ALWAYS_ENDPOINT;
102 |
103 | PushMessage pushMessage = new PushMessage(WALRUS_CONTENT);
104 |
105 | PushServiceClient pushClient = PreparePushServiceClient();
106 | pushClient.MaxRetriesAfter = 1;
107 |
108 | await Assert.ThrowsAsync(async () =>
109 | {
110 | await pushClient.RequestPushMessageDeliveryAsync(_pushSubscription, pushMessage);
111 | });
112 | }
113 |
114 | [Fact]
115 | public async Task PushService_TooManyRequests_MaxRetriesAfter_PushServiceClientExceptionStatusCodeIs429()
116 | {
117 | _pushSubscription.Endpoint = RETRY_AFTER_ALWAYS_ENDPOINT;
118 |
119 | PushMessage pushMessage = new PushMessage(WALRUS_CONTENT);
120 |
121 | PushServiceClient pushClient = PreparePushServiceClient();
122 | pushClient.MaxRetriesAfter = 1;
123 |
124 | PushServiceClientException pushMessageDeliveryException = await Record.ExceptionAsync(async () =>
125 | {
126 | await pushClient.RequestPushMessageDeliveryAsync(_pushSubscription, pushMessage);
127 | }) as PushServiceClientException;
128 |
129 | Assert.Equal(429, (int)pushMessageDeliveryException.StatusCode);
130 | }
131 |
132 | [Fact]
133 | public async Task PushService_OtherClientError_ThrowsPushServiceClientException()
134 | {
135 | _pushSubscription.Endpoint = CLIENT_ERROR_ENDPOINT;
136 |
137 | PushMessage pushMessage = new PushMessage(WALRUS_CONTENT);
138 |
139 | PushServiceClient pushClient = PreparePushServiceClient();
140 |
141 | await Assert.ThrowsAsync(async () =>
142 | {
143 | await pushClient.RequestPushMessageDeliveryAsync(_pushSubscription, pushMessage);
144 | });
145 | }
146 |
147 | [Fact]
148 | public async Task PushService_OtherClientError_PushServiceClientExceptionContainsResponseStatusCode()
149 | {
150 | _pushSubscription.Endpoint = CLIENT_ERROR_ENDPOINT;
151 |
152 | PushMessage pushMessage = new PushMessage(WALRUS_CONTENT);
153 |
154 | PushServiceClient pushClient = PreparePushServiceClient();
155 |
156 | PushServiceClientException pushMessageDeliveryException = await Record.ExceptionAsync(async () =>
157 | {
158 | await pushClient.RequestPushMessageDeliveryAsync(_pushSubscription, pushMessage);
159 | }) as PushServiceClientException;
160 |
161 | Assert.Equal(FakePushServiceStartup.OTHER_CLIENT_ERROR_STATUS_CODE, (int)pushMessageDeliveryException.StatusCode);
162 | }
163 |
164 | [Fact]
165 | public async Task PushService_OtherClientError_PushServiceClientExceptionContainsResponseReasonPhrase()
166 | {
167 | _pushSubscription.Endpoint = CLIENT_ERROR_ENDPOINT;
168 |
169 | PushMessage pushMessage = new PushMessage(WALRUS_CONTENT);
170 |
171 | PushServiceClient pushClient = PreparePushServiceClient();
172 |
173 | PushServiceClientException pushMessageDeliveryException = await Record.ExceptionAsync(async () =>
174 | {
175 | await pushClient.RequestPushMessageDeliveryAsync(_pushSubscription, pushMessage);
176 | }) as PushServiceClientException;
177 |
178 | Assert.Equal(FakePushServiceStartup.OTHER_CLIENT_ERROR_REASON_PHRASE, pushMessageDeliveryException.Message);
179 | }
180 |
181 | [Fact]
182 | public async Task PushService_OtherClientError_PushServiceClientExceptionContainsResponseBody()
183 | {
184 | _pushSubscription.Endpoint = CLIENT_ERROR_ENDPOINT;
185 |
186 | PushMessage pushMessage = new PushMessage(WALRUS_CONTENT);
187 |
188 | PushServiceClient pushClient = PreparePushServiceClient();
189 |
190 | PushServiceClientException pushMessageDeliveryException = await Record.ExceptionAsync(async () =>
191 | {
192 | await pushClient.RequestPushMessageDeliveryAsync(_pushSubscription, pushMessage);
193 | }) as PushServiceClientException;
194 |
195 | Assert.Equal(FakePushServiceStartup.OTHER_CLIENT_ERROR_BODY, pushMessageDeliveryException.Body);
196 | }
197 |
198 | [Fact]
199 | public async Task PushService_OtherClientError_PushServiceClientExceptionContainsPushSubscription()
200 | {
201 | _pushSubscription.Endpoint = CLIENT_ERROR_ENDPOINT;
202 |
203 | PushMessage pushMessage = new PushMessage(WALRUS_CONTENT);
204 |
205 | PushServiceClient pushClient = PreparePushServiceClient();
206 |
207 | PushServiceClientException pushMessageDeliveryException = await Record.ExceptionAsync(async () =>
208 | {
209 | await pushClient.RequestPushMessageDeliveryAsync(_pushSubscription, pushMessage);
210 | }) as PushServiceClientException;
211 |
212 | Assert.Equal(_pushSubscription, pushMessageDeliveryException.PushSubscription);
213 | }
214 |
215 | [Fact]
216 | public async Task PushService_PushEncryptionKeysNamesLowercase_DeliversPushMessage()
217 | {
218 | PushSubscription pushSubscription = new PushSubscription
219 | {
220 | Keys = new Dictionary
221 | {
222 | { "auth", PUSH_SUBSCRIPTION_AUTH_KEY },
223 | { "p256dh", PUSH_SUBSCRIPTION_P256DH_KEY }
224 | },
225 | Endpoint = RFC_8030_CREATED_ENDPOINT
226 | };
227 |
228 | PushMessage pushMessage = new PushMessage(WALRUS_CONTENT);
229 |
230 | PushServiceClient pushClient = PreparePushServiceClient();
231 |
232 | Exception pushMessageDeliveryException = await Record.ExceptionAsync(async () =>
233 | {
234 | await pushClient.RequestPushMessageDeliveryAsync(pushSubscription, pushMessage);
235 | });
236 |
237 | Assert.Null(pushMessageDeliveryException);
238 | }
239 |
240 | [Fact]
241 | public async Task PushService_PushEncryptionKeysNamesUppercase_DeliversPushMessage()
242 | {
243 | PushSubscription pushSubscription = new PushSubscription
244 | {
245 | Keys = new Dictionary
246 | {
247 | { "AUTH", PUSH_SUBSCRIPTION_AUTH_KEY },
248 | { "P256DH", PUSH_SUBSCRIPTION_P256DH_KEY }
249 | },
250 | Endpoint = RFC_8030_CREATED_ENDPOINT
251 | };
252 |
253 | PushMessage pushMessage = new PushMessage(WALRUS_CONTENT);
254 |
255 | PushServiceClient pushClient = PreparePushServiceClient();
256 |
257 | Exception pushMessageDeliveryException = await Record.ExceptionAsync(async () =>
258 | {
259 | await pushClient.RequestPushMessageDeliveryAsync(pushSubscription, pushMessage);
260 | });
261 |
262 | Assert.Null(pushMessageDeliveryException);
263 | }
264 |
265 | [Fact]
266 | public async Task PushService_PushEncryptionKeysNamesMixedCase_DeliversPushMessage()
267 | {
268 | PushSubscription pushSubscription = new PushSubscription
269 | {
270 | Keys = new Dictionary
271 | {
272 | { "AuTh", PUSH_SUBSCRIPTION_AUTH_KEY },
273 | { "P256dH", PUSH_SUBSCRIPTION_P256DH_KEY }
274 | },
275 | Endpoint = RFC_8030_CREATED_ENDPOINT
276 | };
277 |
278 | PushMessage pushMessage = new PushMessage(WALRUS_CONTENT);
279 |
280 | PushServiceClient pushClient = PreparePushServiceClient();
281 |
282 | Exception pushMessageDeliveryException = await Record.ExceptionAsync(async () =>
283 | {
284 | await pushClient.RequestPushMessageDeliveryAsync(pushSubscription, pushMessage);
285 | });
286 |
287 | Assert.Null(pushMessageDeliveryException);
288 | }
289 | #endregion
290 | }
291 | }
292 |
--------------------------------------------------------------------------------
/test/Test.Lib.Net.Http.WebPush/Test.Lib.Net.Http.WebPush.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netcoreapp3.1;net8.0;net9.0
4 | false
5 |
6 |
7 |
8 |
9 |
10 | all
11 | runtime; build; native; contentfiles; analyzers; buildtransitive
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/test/Test.Lib.Net.Http.WebPush/Unit/PushSubscriptionTests.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Xunit;
3 | using Lib.Net.Http.WebPush;
4 |
5 | namespace Test.Lib.Net.Http.WebPush.Unit
6 | {
7 | public class PushSubscriptionTests
8 | {
9 | #region Fields
10 | private const string AUTH_KEY = "n5mG_PyMSKALsjsU542E6g";
11 | private const string P256DH_KEY = "BDS52l6tfaf6ZEqhyDa0cScvCi4WXNYPIwmfas-7nKLIQex-DVKXB9gUxDExaZEOiwovl6LbWXZBZ9AT-GWT6eQ";
12 | #endregion
13 |
14 | #region Tests
15 | [Fact]
16 | public void GetKey_AuthKeyNameLowercase_ReturnsKey()
17 | {
18 | PushSubscription pushSubscription = new PushSubscription
19 | {
20 | Keys = new Dictionary
21 | {
22 | { "auth", AUTH_KEY }
23 | }
24 | };
25 |
26 | string authKey = pushSubscription.GetKey(PushEncryptionKeyName.Auth);
27 |
28 | Assert.Equal(AUTH_KEY, authKey);
29 | }
30 |
31 | [Fact]
32 | public void GetKey_AuthKeyNameUppercase_ReturnsKey()
33 | {
34 | PushSubscription pushSubscription = new PushSubscription
35 | {
36 | Keys = new Dictionary
37 | {
38 | { "AUTH", AUTH_KEY }
39 | }
40 | };
41 |
42 | string authKey = pushSubscription.GetKey(PushEncryptionKeyName.Auth);
43 |
44 | Assert.Equal(AUTH_KEY, authKey);
45 | }
46 |
47 | [Fact]
48 | public void GetKey_AuthKeyNameMixedCase_ReturnsKey()
49 | {
50 | PushSubscription pushSubscription = new PushSubscription
51 | {
52 | Keys = new Dictionary
53 | {
54 | { "AuTh", AUTH_KEY }
55 | }
56 | };
57 |
58 | string authKey = pushSubscription.GetKey(PushEncryptionKeyName.Auth);
59 |
60 | Assert.Equal(AUTH_KEY, authKey);
61 | }
62 |
63 | [Fact]
64 | public void GetKey_P256DHKeyNameLowercase_ReturnsKey()
65 | {
66 | PushSubscription pushSubscription = new PushSubscription
67 | {
68 | Keys = new Dictionary
69 | {
70 | { "p256dh", P256DH_KEY }
71 | }
72 | };
73 |
74 | string p256dhKey = pushSubscription.GetKey(PushEncryptionKeyName.P256DH);
75 |
76 | Assert.Equal(P256DH_KEY, p256dhKey);
77 | }
78 |
79 | [Fact]
80 | public void GetKey_P256DHKeyNameUppercase_ReturnsKey()
81 | {
82 | PushSubscription pushSubscription = new PushSubscription
83 | {
84 | Keys = new Dictionary
85 | {
86 | { "P256DH", P256DH_KEY }
87 | }
88 | };
89 |
90 | string p256dhKey = pushSubscription.GetKey(PushEncryptionKeyName.P256DH);
91 |
92 | Assert.Equal(P256DH_KEY, p256dhKey);
93 | }
94 |
95 | [Fact]
96 | public void GetKey_P256DHKeyNameMixedCase_ReturnsKey()
97 | {
98 | PushSubscription pushSubscription = new PushSubscription
99 | {
100 | Keys = new Dictionary
101 | {
102 | { "P256dH", P256DH_KEY }
103 | }
104 | };
105 |
106 | string p256dhKey = pushSubscription.GetKey(PushEncryptionKeyName.P256DH);
107 |
108 | Assert.Equal(P256DH_KEY, p256dhKey);
109 | }
110 | #endregion
111 | }
112 | }
113 |
--------------------------------------------------------------------------------