├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── WebPush.NetCore.Test ├── ECKeyHelperTest.cs ├── JWSSignerTest.cs ├── UrlBase64Test.cs ├── VapidHelperTest.cs ├── WebPush.NetCore.Test.csproj └── WebPushClientTest.cs ├── WebPush.NetCore.sln ├── WebPush.NetCore ├── Model │ ├── EncryptionResult.cs │ ├── PushSubscription.cs │ ├── VapidDetails.cs │ └── WebPushException.cs ├── Util │ ├── ECKeyHelper.cs │ ├── Encryptor.cs │ ├── JWSSigner.cs │ └── UrlBase64.cs ├── VapidHelper.cs ├── WebPush.NetCore.csproj └── WebPushClient.cs └── build.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /*.suo 2 | 3 | 4 | Debug 5 | .vs 6 | Build 7 | bin 8 | obj 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | dist: trusty 3 | sudo: required 4 | mono: none 5 | dotnet: 1.0.4 6 | before_install: 7 | - chmod +x build.sh 8 | script: 9 | - ./build.sh --quiet verify 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### As the original repo is supported Dotnet core already, I guess this repo can be archived now. 2 | ### Please go the [Original repo](https://github.com/web-push-libs/web-push-csharp) and view the [ASP.NET MVC Core Example](https://github.com/coryjthompson/WebPushDemo) 3 | 4 | ``` 5 | # WebPush.NetCore 6 | ![](https://travis-ci.org/vip30/WebPush-NetCore.svg?branch=master) 7 | [![NuGet](https://img.shields.io/nuget/v/WebPush-NetCore.svg)](https://www.nuget.org/packages/WebPush-NetCore/) 8 | 9 | A Brief Intro 10 | ------------------- 11 | 12 | WebPush .NetCore version 13 | 14 | [Original repo](https://github.com/web-push-libs/web-push-csharp) 15 | 16 | [Visit Nuget](https://www.nuget.org/packages/WebPush-NetCore) 17 | 18 | 19 | Usage 20 | ------------------- 21 | Same as the originial version of web-push-csharp 22 | ```cs 23 | using WebPush; 24 | 25 | var pushEndpoint = @"https://fcm.googleapis.com/fcm/send/efz_TLX_rLU:APA91bE6U0iybLYvv0F3mf6uDLB6...."; 26 | var p256dh = @"BKK18ZjtENC4jdhAAg9OfJacySQiDVcXMamy3SKKy7FwJcI5E0DKO9v4V2Pb8NnAPN4EVdmhO............"; 27 | var auth = @"fkJatBBEl..............."; 28 | 29 | var subject = @"mailto:example@example.com"; 30 | var publicKey = @"BDjASz8kkVBQJgWcD05uX3VxIs_gSHyuS023jnBoHBgUbg8zIJvTSQytR8MP4Z3-kzcGNVnM..............."; 31 | var privateKey = @"mryM-krWj_6IsIMGsd8wNFXGBxnx..............."; 32 | 33 | var subscription = new PushSubscription(pushEndpoint, p256dh, auth); 34 | var vapidDetails = new VapidDetails(subject, publicKey, privateKey); 35 | //var gcmAPIKey = @"[your key here]"; 36 | 37 | var webPushClient = new WebPushClient(); 38 | try 39 | { 40 | webPushClient.SendNotification(subscription, "payload", vapidDetails); 41 | //webPushClient.SendNotification(subscription, "payload", gcmAPIKey); 42 | } 43 | catch (WebPushException exception) 44 | { 45 | Console.WriteLine("Http STATUS code" + exception.StatusCode); 46 | } 47 | # Credits 48 | - Ported from https://github.com/web-push-libs/web-push-csharp. 49 | ``` 50 | -------------------------------------------------------------------------------- /WebPush.NetCore.Test/ECKeyHelperTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using Org.BouncyCastle.Crypto; 3 | using Org.BouncyCastle.Crypto.Parameters; 4 | using System.Linq; 5 | using WebPush.Util; 6 | 7 | namespace WebPush.Test 8 | { 9 | [TestClass] 10 | public class ECKeyHelperTest 11 | { 12 | private const string TEST_PRIVATE_KEY = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI"; 13 | 14 | private const string TEST_PUBLIC_KEY = 15 | @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33yJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A"; 16 | 17 | [TestMethod] 18 | public void TestGenerateKeys() 19 | { 20 | AsymmetricCipherKeyPair keys = ECKeyHelper.GenerateKeys(); 21 | 22 | byte[] publicKey = ((ECPublicKeyParameters)keys.Public).Q.GetEncoded(false); 23 | byte[] privateKey = ((ECPrivateKeyParameters)keys.Private).D.ToByteArrayUnsigned(); 24 | 25 | int publicKeyLength = publicKey.Length; 26 | int privateKeyLength = privateKey.Length; 27 | 28 | Assert.AreEqual(65, publicKeyLength); 29 | Assert.AreEqual(32, privateKeyLength); 30 | 31 | ; 32 | } 33 | 34 | [TestMethod] 35 | public void TestGenerateKeysNoCache() 36 | { 37 | AsymmetricCipherKeyPair keys1 = ECKeyHelper.GenerateKeys(); 38 | AsymmetricCipherKeyPair keys2 = ECKeyHelper.GenerateKeys(); 39 | 40 | byte[] publicKey1 = ((ECPublicKeyParameters)keys1.Public).Q.GetEncoded(false); 41 | byte[] privateKey1 = ((ECPrivateKeyParameters)keys1.Private).D.ToByteArrayUnsigned(); 42 | 43 | byte[] publicKey2 = ((ECPublicKeyParameters)keys2.Public).Q.GetEncoded(false); 44 | byte[] privateKey2 = ((ECPrivateKeyParameters)keys2.Private).D.ToByteArrayUnsigned(); 45 | 46 | Assert.IsFalse(publicKey1.SequenceEqual(publicKey2)); 47 | Assert.IsFalse(privateKey1.SequenceEqual(privateKey2)); 48 | } 49 | 50 | [TestMethod] 51 | public void TestGetPrivateKey() 52 | { 53 | byte[] privateKey = UrlBase64.Decode(TEST_PRIVATE_KEY); 54 | ECPrivateKeyParameters privateKeyParams = ECKeyHelper.GetPrivateKey(privateKey); 55 | 56 | string importedPrivateKey = UrlBase64.Encode(privateKeyParams.D.ToByteArrayUnsigned()); 57 | 58 | Assert.AreEqual(TEST_PRIVATE_KEY, importedPrivateKey); 59 | } 60 | 61 | [TestMethod] 62 | public void TestGetPublicKey() 63 | { 64 | byte[] publicKey = UrlBase64.Decode(TEST_PUBLIC_KEY); 65 | ECPublicKeyParameters publicKeyParams = ECKeyHelper.GetPublicKey(publicKey); 66 | 67 | string importedPublicKey = UrlBase64.Encode(publicKeyParams.Q.GetEncoded(false)); 68 | 69 | Assert.AreEqual(TEST_PUBLIC_KEY, importedPublicKey); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /WebPush.NetCore.Test/JWSSignerTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | using Org.BouncyCastle.Crypto.Parameters; 8 | using WebPush.Util; 9 | 10 | namespace WebPush.Test 11 | { 12 | [TestClass] 13 | public class JWSSignerTest 14 | { 15 | [TestMethod] 16 | public void TestGenerateSignature() 17 | { 18 | ECPrivateKeyParameters privateKey = ECKeyHelper.GetPrivateKey(new byte[32]); 19 | 20 | Dictionary header = new Dictionary(); 21 | header.Add("typ", "JWT"); 22 | header.Add("alg", "ES256"); 23 | 24 | Dictionary jwtPayload = new Dictionary(); 25 | jwtPayload.Add("aud", "aud"); 26 | jwtPayload.Add("exp", 1); 27 | jwtPayload.Add("sub", "subject"); 28 | 29 | JWSSigner signer = new JWSSigner(privateKey); 30 | string token = signer.GenerateSignature(header, jwtPayload); 31 | 32 | string[] tokenParts = token.Split('.'); 33 | 34 | Assert.AreEqual(3, tokenParts.Length); 35 | 36 | string encodedHeader = tokenParts[0]; 37 | string encodedPayload = tokenParts[1]; 38 | string signature = tokenParts[2]; 39 | 40 | string decodedHeader = Encoding.UTF8.GetString(UrlBase64.Decode(encodedHeader)); 41 | string decodedPayload = Encoding.UTF8.GetString(UrlBase64.Decode(encodedPayload)); 42 | 43 | Assert.AreEqual(@"{""typ"":""JWT"",""alg"":""ES256""}", decodedHeader); 44 | Assert.AreEqual(@"{""aud"":""aud"",""exp"":1,""sub"":""subject""}", decodedPayload); 45 | 46 | byte[] decodedSignature = UrlBase64.Decode(signature); 47 | int decodedSignatureLength = decodedSignature.Length; 48 | 49 | 50 | bool isSignatureLengthValid = decodedSignatureLength == 66 || decodedSignatureLength == 64; 51 | Assert.AreEqual(true, isSignatureLengthValid); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /WebPush.NetCore.Test/UrlBase64Test.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System.Linq; 3 | using WebPush.Util; 4 | 5 | namespace WebPush.Test 6 | { 7 | [TestClass] 8 | public class UrlBase64Test 9 | { 10 | [TestMethod] 11 | public void TestBase64UrlDecode() 12 | { 13 | byte[] expected = new byte[3] { 181, 235, 45 }; 14 | byte[] actual = UrlBase64.Decode(@"test"); 15 | Assert.IsTrue(actual.SequenceEqual(expected)); 16 | } 17 | 18 | [TestMethod] 19 | public void TestBase64UrlEncode() 20 | { 21 | string expected = @"test"; 22 | string actual = UrlBase64.Encode(new byte[3] { 181, 235, 45 }); 23 | Assert.AreEqual(expected, actual); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /WebPush.NetCore.Test/VapidHelperTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Collections.Generic; 4 | using WebPush.Util; 5 | 6 | namespace WebPush.Test 7 | { 8 | [TestClass] 9 | public class VapidHelperTest 10 | { 11 | private const string VALID_AUDIENCE = "http://example.com"; 12 | private const string VALID_SUBJECT = "http://example.com/example"; 13 | private const string VALID_SUBJECT_MAILTO = "mailto:example@example.com"; 14 | 15 | [TestMethod] 16 | public void TestGenerateVapidKeys() 17 | { 18 | VapidDetails keys = VapidHelper.GenerateVapidKeys(); 19 | byte[] publicKey = UrlBase64.Decode(keys.PublicKey); 20 | byte[] privateKey = UrlBase64.Decode(keys.PrivateKey); 21 | 22 | Assert.AreEqual(32, privateKey.Length); 23 | Assert.AreEqual(65, publicKey.Length); 24 | } 25 | 26 | [TestMethod] 27 | public void TestGenerateVapidKeysNoCache() 28 | { 29 | VapidDetails keys1 = VapidHelper.GenerateVapidKeys(); 30 | VapidDetails keys2 = VapidHelper.GenerateVapidKeys(); 31 | 32 | Assert.AreNotEqual(keys1.PublicKey, keys2.PublicKey); 33 | Assert.AreNotEqual(keys1.PrivateKey, keys2.PrivateKey); 34 | } 35 | 36 | [TestMethod] 37 | public void TestGetVapidHeaders() 38 | { 39 | string publicKey = UrlBase64.Encode(new byte[65]); 40 | string privatekey = UrlBase64.Encode(new byte[32]); 41 | Dictionary headers = VapidHelper.GetVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT, publicKey, privatekey); 42 | 43 | Assert.IsTrue(headers.ContainsKey("Authorization")); 44 | Assert.IsTrue(headers.ContainsKey("Crypto-Key")); 45 | } 46 | 47 | [TestMethod] 48 | public void TestGetVapidHeadersAudienceNotAUrl() 49 | { 50 | string publicKey = UrlBase64.Encode(new byte[65]); 51 | string privatekey = UrlBase64.Encode(new byte[32]); 52 | 53 | Assert.ThrowsException( 54 | delegate 55 | { 56 | VapidHelper.GetVapidHeaders("invalid audience", VALID_SUBJECT, publicKey, privatekey); 57 | }); 58 | } 59 | 60 | [TestMethod] 61 | public void TestGetVapidHeadersInvalidPrivateKey() 62 | { 63 | string publicKey = UrlBase64.Encode(new byte[65]); 64 | string privatekey = UrlBase64.Encode(new byte[1]); 65 | 66 | Assert.ThrowsException( 67 | delegate 68 | { 69 | VapidHelper.GetVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT, publicKey, privatekey); 70 | }); 71 | } 72 | 73 | [TestMethod] 74 | public void TestGetVapidHeadersInvalidPublicKey() 75 | { 76 | string publicKey = UrlBase64.Encode(new byte[1]); 77 | string privatekey = UrlBase64.Encode(new byte[32]); 78 | 79 | Assert.ThrowsException( 80 | delegate 81 | { 82 | VapidHelper.GetVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT, publicKey, privatekey); 83 | }); 84 | } 85 | 86 | [TestMethod] 87 | public void TestGetVapidHeadersSubjectNotAUrlOrMailTo() 88 | { 89 | string publicKey = UrlBase64.Encode(new byte[65]); 90 | string privatekey = UrlBase64.Encode(new byte[32]); 91 | 92 | Assert.ThrowsException( 93 | delegate 94 | { 95 | VapidHelper.GetVapidHeaders(VALID_AUDIENCE, "invalid subject", publicKey, privatekey); 96 | }); 97 | } 98 | 99 | [TestMethod] 100 | public void TestGetVapidHeadersWithMailToSubject() 101 | { 102 | string publicKey = UrlBase64.Encode(new byte[65]); 103 | string privatekey = UrlBase64.Encode(new byte[32]); 104 | Dictionary headers = VapidHelper.GetVapidHeaders(VALID_AUDIENCE, VALID_SUBJECT_MAILTO, publicKey, 105 | privatekey); 106 | 107 | Assert.IsTrue(headers.ContainsKey("Authorization")); 108 | Assert.IsTrue(headers.ContainsKey("Crypto-Key")); 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /WebPush.NetCore.Test/WebPush.NetCore.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp1.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /WebPush.NetCore.Test/WebPushClientTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net.Http; 6 | 7 | namespace WebPush.NetCore.Test 8 | { 9 | [TestClass] 10 | public class WebPushClientTest 11 | { 12 | private const string TEST_FCM_ENDPOINT = 13 | @"https://fcm.googleapis.com/fcm/send/efz_TLX_rLU:APA91bE6U0iybLYvv0F3mf6"; 14 | 15 | private const string TEST_GCM_ENDPOINT = @"https://android.googleapis.com/gcm/send/"; 16 | 17 | private const string TEST_PRIVATE_KEY = @"on6X5KmLEFIVvPP3cNX9kE0OF6PV9TJQXVbnKU2xEHI"; 18 | 19 | private const string TEST_PUBLIC_KEY = 20 | @"BCvKwB2lbVUYMFAaBUygooKheqcEU-GDrVRnu8k33yJCZkNBNqjZj0VdxQ2QIZa4kV5kpX9aAqyBKZHURm6eG1A"; 21 | 22 | [TestMethod] 23 | public void TestGCMAPIKeyInOptions() 24 | { 25 | WebPushClient client = new WebPushClient(); 26 | 27 | string gcmAPIKey = @"teststring"; 28 | PushSubscription subscription = new PushSubscription(TEST_GCM_ENDPOINT, TEST_PUBLIC_KEY, TEST_PRIVATE_KEY); 29 | 30 | Dictionary options = new Dictionary(); 31 | options["gcmAPIKey"] = gcmAPIKey; 32 | HttpRequestMessage message = client.GenerateRequestDetails(subscription, "test payload", options); 33 | string authorizationHeader = message.Headers.GetValues("Authorization").First(); 34 | 35 | Assert.AreEqual("key=" + gcmAPIKey, authorizationHeader); 36 | 37 | // Test previous incorrect casing of gcmAPIKey 38 | Dictionary options2 = new Dictionary(); 39 | options2["gcmApiKey"] = gcmAPIKey; 40 | Assert.ThrowsException(delegate 41 | { 42 | client.GenerateRequestDetails(subscription, "test payload", options2); 43 | }); 44 | } 45 | 46 | [TestMethod] 47 | public void TestSendFCMMessage() 48 | { 49 | string p256dh = @"BPez73CdNHyBIFW"; 50 | string auth = @"r_36Ti2Z"; 51 | WebPushClient client = new WebPushClient(); 52 | PushSubscription subscription = new PushSubscription(TEST_FCM_ENDPOINT, p256dh, auth); 53 | var vapidDetails = new VapidDetails("mailto:example@example.com", TEST_PUBLIC_KEY, TEST_PRIVATE_KEY); 54 | client.SendNotification(subscription, "Insert here a payload", vapidDetails); 55 | } 56 | 57 | [TestMethod] 58 | public void TestSetGCMAPIKey() 59 | { 60 | WebPushClient client = new WebPushClient(); 61 | 62 | string gcmAPIKey = @"teststring"; 63 | client.SetGCMAPIKey(gcmAPIKey); 64 | PushSubscription subscription = new PushSubscription(TEST_GCM_ENDPOINT, TEST_PUBLIC_KEY, TEST_PRIVATE_KEY); 65 | HttpRequestMessage message = client.GenerateRequestDetails(subscription, "test payload"); 66 | string authorizationHeader = message.Headers.GetValues("Authorization").First(); 67 | 68 | Assert.AreEqual("key=" + gcmAPIKey, authorizationHeader); 69 | } 70 | 71 | [TestMethod] 72 | public void TestSetGCMAPIKeyEmptyString() 73 | { 74 | WebPushClient client = new WebPushClient(); 75 | 76 | Assert.ThrowsException(delegate 77 | { 78 | client.SetGCMAPIKey(""); 79 | }); 80 | } 81 | 82 | [TestMethod] 83 | public void TestSetGCMAPiKeyNonGCMPushService() 84 | { 85 | // Ensure that the API key doesn't get added on a service that doesn't accept it. 86 | WebPushClient client = new WebPushClient(); 87 | 88 | string gcmAPIKey = @"teststring"; 89 | client.SetGCMAPIKey(gcmAPIKey); 90 | PushSubscription subscription = new PushSubscription(TEST_FCM_ENDPOINT, TEST_PUBLIC_KEY, TEST_PRIVATE_KEY); 91 | HttpRequestMessage message = client.GenerateRequestDetails(subscription, "test payload"); 92 | 93 | IEnumerable values; 94 | Assert.IsFalse(message.Headers.TryGetValues("Authorization", out values)); 95 | } 96 | 97 | [TestMethod] 98 | public void TestSetGCMAPIKeyNull() 99 | { 100 | WebPushClient client = new WebPushClient(); 101 | 102 | client.SetGCMAPIKey(@"somestring"); 103 | client.SetGCMAPIKey(null); 104 | 105 | PushSubscription subscription = new PushSubscription(TEST_GCM_ENDPOINT, TEST_PUBLIC_KEY, TEST_PRIVATE_KEY); 106 | HttpRequestMessage message = client.GenerateRequestDetails(subscription, "test payload"); 107 | 108 | IEnumerable values; 109 | Assert.IsFalse(message.Headers.TryGetValues("Authorization", out values)); 110 | } 111 | 112 | [TestMethod] 113 | public void TestSetVapidDetails() 114 | { 115 | WebPushClient client = new WebPushClient(); 116 | 117 | client.SetVapidDetails("mailto:example@example.com", TEST_PUBLIC_KEY, TEST_PRIVATE_KEY); 118 | 119 | PushSubscription subscription = new PushSubscription(TEST_FCM_ENDPOINT, TEST_PUBLIC_KEY, TEST_PRIVATE_KEY); 120 | HttpRequestMessage message = client.GenerateRequestDetails(subscription, "test payload"); 121 | string authorizationHeader = message.Headers.GetValues("Authorization").First(); 122 | string cryptoHeader = message.Headers.GetValues("Crypto-Key").First(); 123 | 124 | Assert.IsTrue(authorizationHeader.StartsWith("WebPush ")); 125 | Assert.IsTrue(cryptoHeader.Contains("p256ecdsa")); 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /WebPush.NetCore.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26403.7 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E36B9A31-8BD5-496F-9844-E5F7AD440B25}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebPush.NetCore.Test", "WebPush.NetCore.Test\WebPush.NetCore.Test.csproj", "{67680059-FA89-4463-9F07-6D4D067A2761}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebPush.NetCore", "WebPush.NetCore\WebPush.NetCore.csproj", "{184AF484-3700-48E5-9764-0E7836739C80}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {67680059-FA89-4463-9F07-6D4D067A2761}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {67680059-FA89-4463-9F07-6D4D067A2761}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {67680059-FA89-4463-9F07-6D4D067A2761}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {67680059-FA89-4463-9F07-6D4D067A2761}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {184AF484-3700-48E5-9764-0E7836739C80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {184AF484-3700-48E5-9764-0E7836739C80}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {184AF484-3700-48E5-9764-0E7836739C80}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {184AF484-3700-48E5-9764-0E7836739C80}.Release|Any CPU.Build.0 = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(NestedProjects) = preSolution 31 | {67680059-FA89-4463-9F07-6D4D067A2761} = {E36B9A31-8BD5-496F-9844-E5F7AD440B25} 32 | {184AF484-3700-48E5-9764-0E7836739C80} = {E36B9A31-8BD5-496F-9844-E5F7AD440B25} 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /WebPush.NetCore/Model/EncryptionResult.cs: -------------------------------------------------------------------------------- 1 | using WebPush.Util; 2 | 3 | // @LogicSoftware 4 | // Originally From: https://github.com/LogicSoftware/WebPushEncryption/blob/master/src/EncryptionResult.cs 5 | namespace WebPush 6 | { 7 | public class EncryptionResult 8 | { 9 | public byte[] PublicKey { get; set; } 10 | public byte[] Payload { get; set; } 11 | public byte[] Salt { get; set; } 12 | 13 | public string Base64EncodePublicKey() 14 | { 15 | return UrlBase64.Encode(PublicKey); 16 | } 17 | 18 | public string Base64EncodeSalt() 19 | { 20 | return UrlBase64.Encode(Salt); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /WebPush.NetCore/Model/PushSubscription.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace WebPush 8 | { 9 | public class PushSubscription 10 | { 11 | public string Endpoint { get; set; } 12 | public string P256DH { get; set; } 13 | public string Auth { get; set; } 14 | 15 | public PushSubscription() 16 | { 17 | 18 | } 19 | 20 | public PushSubscription(string endpoint, string p256dh, string auth) 21 | { 22 | Endpoint = endpoint; 23 | P256DH = p256dh; 24 | Auth = auth; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /WebPush.NetCore/Model/VapidDetails.cs: -------------------------------------------------------------------------------- 1 | namespace WebPush 2 | { 3 | public class VapidDetails 4 | { 5 | public string Subject { get; set; } 6 | public string PublicKey { get; set; } 7 | public string PrivateKey { get; set; } 8 | 9 | public VapidDetails() 10 | { 11 | 12 | } 13 | 14 | /// This should be a URL or a 'mailto:' email address 15 | /// The VAPID public key as a base64 encoded string 16 | /// The VAPID private key as a base64 encoded string 17 | public VapidDetails(string subject, string publicKey, string privateKey) 18 | { 19 | Subject = subject; 20 | PublicKey = publicKey; 21 | PrivateKey = privateKey; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /WebPush.NetCore/Model/WebPushException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http.Headers; 4 | 5 | namespace WebPush 6 | { 7 | public class WebPushException : Exception 8 | { 9 | public HttpStatusCode StatusCode { get; set; } 10 | public HttpResponseHeaders Headers { get; set; } 11 | public PushSubscription PushSubscription { get; set; } 12 | 13 | public WebPushException(string message, HttpStatusCode statusCode, HttpResponseHeaders headers, PushSubscription pushSubscription) : base(message) 14 | { 15 | StatusCode = statusCode; 16 | Headers = headers; 17 | PushSubscription = pushSubscription; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /WebPush.NetCore/Util/ECKeyHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Org.BouncyCastle.Asn1; 4 | using Org.BouncyCastle.Asn1.Nist; 5 | using Org.BouncyCastle.Asn1.X9; 6 | using Org.BouncyCastle.Crypto; 7 | using Org.BouncyCastle.Crypto.Generators; 8 | using Org.BouncyCastle.Crypto.Parameters; 9 | using Org.BouncyCastle.OpenSsl; 10 | using Org.BouncyCastle.Security; 11 | 12 | namespace WebPush.Util 13 | { 14 | public static class ECKeyHelper 15 | { 16 | public static ECPrivateKeyParameters GetPrivateKey(byte[] privateKey) 17 | { 18 | Asn1Object version = new DerInteger(1); 19 | Asn1Object derEncodedKey = new DerOctetString(privateKey); 20 | Asn1Object keyTypeParameters = new DerTaggedObject(0, new DerObjectIdentifier(@"1.2.840.10045.3.1.7")); 21 | 22 | Asn1Object derSequence = new DerSequence(version, derEncodedKey, keyTypeParameters); 23 | 24 | var base64EncodedDerSequence = Convert.ToBase64String(derSequence.GetDerEncoded()); 25 | var pemKey = "-----BEGIN EC PRIVATE KEY-----\n"; 26 | pemKey += base64EncodedDerSequence; 27 | pemKey += "\n-----END EC PRIVATE KEY----"; 28 | 29 | StringReader reader = new StringReader(pemKey); 30 | PemReader pemReader = new PemReader(reader); 31 | AsymmetricCipherKeyPair keyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject(); 32 | 33 | return (ECPrivateKeyParameters)keyPair.Private; 34 | } 35 | 36 | public static ECPublicKeyParameters GetPublicKey(byte[] publicKey) 37 | { 38 | Asn1Object keyTypeParameters = new DerSequence(new DerObjectIdentifier(@"1.2.840.10045.2.1"), new DerObjectIdentifier(@"1.2.840.10045.3.1.7")); 39 | Asn1Object derEncodedKey = new DerBitString(publicKey); 40 | 41 | Asn1Object derSequence = new DerSequence(keyTypeParameters, derEncodedKey); 42 | 43 | var base64EncodedDerSequence = Convert.ToBase64String(derSequence.GetDerEncoded()); 44 | var pemKey = "-----BEGIN PUBLIC KEY-----\n"; 45 | pemKey += base64EncodedDerSequence; 46 | pemKey += "\n-----END PUBLIC KEY-----"; 47 | 48 | StringReader reader = new StringReader(pemKey); 49 | PemReader pemReader = new PemReader(reader); 50 | var keyPair = pemReader.ReadObject(); 51 | return (ECPublicKeyParameters)keyPair; 52 | } 53 | 54 | public static AsymmetricCipherKeyPair GenerateKeys() 55 | { 56 | X9ECParameters ecParameters = NistNamedCurves.GetByName("P-256"); 57 | ECDomainParameters ecSpec = new ECDomainParameters(ecParameters.Curve, ecParameters.G, ecParameters.N, ecParameters.H, ecParameters.GetSeed()); 58 | IAsymmetricCipherKeyPairGenerator keyPairGenerator = GeneratorUtilities.GetKeyPairGenerator("ECDH"); 59 | keyPairGenerator.Init(new ECKeyGenerationParameters(ecSpec, new SecureRandom())); 60 | 61 | return keyPairGenerator.GenerateKeyPair(); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /WebPush.NetCore/Util/Encryptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Security.Cryptography; 6 | using System.Text; 7 | using Org.BouncyCastle.Crypto; 8 | using Org.BouncyCastle.Crypto.Engines; 9 | using Org.BouncyCastle.Crypto.Modes; 10 | using Org.BouncyCastle.Crypto.Parameters; 11 | using Org.BouncyCastle.Security; 12 | 13 | // @LogicSoftware 14 | // Originally from https://github.com/LogicSoftware/WebPushEncryption/blob/master/src/Encryptor.cs 15 | namespace WebPush.Util 16 | { 17 | public static class Encryptor 18 | { 19 | private static readonly RandomNumberGenerator RandomNumberProvider = RandomNumberGenerator.Create(); 20 | 21 | public static EncryptionResult Encrypt(string userKey, string userSecret, string payload) 22 | { 23 | byte[] userKeyBytes = UrlBase64.Decode(userKey); 24 | byte[] userSecretBytes = UrlBase64.Decode(userSecret); 25 | byte[] payloadBytes = Encoding.UTF8.GetBytes(payload); 26 | 27 | return Encrypt(userKeyBytes, userSecretBytes, payloadBytes); 28 | } 29 | 30 | public static EncryptionResult Encrypt(byte[] userKey, byte[] userSecret, byte[] payload) 31 | { 32 | byte[] salt = GenerateSalt(16); 33 | AsymmetricCipherKeyPair serverKeyPair = ECKeyHelper.GenerateKeys(); 34 | 35 | IBasicAgreement ecdhAgreement = AgreementUtilities.GetBasicAgreement("ECDH"); 36 | ecdhAgreement.Init(serverKeyPair.Private); 37 | 38 | ECPublicKeyParameters userPublicKey = ECKeyHelper.GetPublicKey(userKey); 39 | 40 | byte[] key = ecdhAgreement.CalculateAgreement(userPublicKey).ToByteArrayUnsigned(); 41 | byte[] serverPublicKey = ((ECPublicKeyParameters)serverKeyPair.Public).Q.GetEncoded(false); 42 | 43 | byte[] prk = HKDF(userSecret, key, Encoding.UTF8.GetBytes("Content-Encoding: auth\0"), 32); 44 | byte[] cek = HKDF(salt, prk, CreateInfoChunk("aesgcm", userKey, serverPublicKey), 16); 45 | byte[] nonce = HKDF(salt, prk, CreateInfoChunk("nonce", userKey, serverPublicKey), 12); 46 | 47 | byte[] input = AddPaddingToInput(payload); 48 | byte[] encryptedMessage = EncryptAes(nonce, cek, input); 49 | 50 | return new EncryptionResult 51 | { 52 | Salt = salt, 53 | Payload = encryptedMessage, 54 | PublicKey = serverPublicKey 55 | }; 56 | } 57 | 58 | private static byte[] GenerateSalt(int length) 59 | { 60 | byte[] salt = new byte[length]; 61 | RandomNumberProvider.GetBytes(salt); 62 | return salt; 63 | } 64 | 65 | private static byte[] AddPaddingToInput(byte[] data) 66 | { 67 | byte[] input = new byte[0 + 2 + data.Length]; 68 | Buffer.BlockCopy(ConvertInt(0), 0, input, 0, 2); 69 | Buffer.BlockCopy(data, 0, input, 0 + 2, data.Length); 70 | return input; 71 | } 72 | 73 | private static byte[] EncryptAes(byte[] nonce, byte[] cek, byte[] message) 74 | { 75 | GcmBlockCipher cipher = new GcmBlockCipher(new AesFastEngine()); 76 | AeadParameters parameters = new AeadParameters(new KeyParameter(cek), 128, nonce); 77 | cipher.Init(true, parameters); 78 | 79 | //Generate Cipher Text With Auth Tag 80 | byte[] cipherText = new byte[cipher.GetOutputSize(message.Length)]; 81 | int len = cipher.ProcessBytes(message, 0, message.Length, cipherText, 0); 82 | cipher.DoFinal(cipherText, len); 83 | 84 | //byte[] tag = cipher.GetMac(); 85 | return cipherText; 86 | } 87 | 88 | public static byte[] HKDFSecondStep(byte[] key, byte[] info, int length) 89 | { 90 | HMACSHA256 hmac = new HMACSHA256(key); 91 | byte[] infoAndOne = info.Concat(new byte[] { 0x01 }).ToArray(); 92 | byte[] result = hmac.ComputeHash(infoAndOne); 93 | 94 | if (result.Length > length) Array.Resize(ref result, length); 95 | return result; 96 | } 97 | 98 | public static byte[] HKDF(byte[] salt, byte[] prk, byte[] info, int length) 99 | { 100 | HMACSHA256 hmac = new HMACSHA256(salt); 101 | byte[] key = hmac.ComputeHash(prk); 102 | 103 | return HKDFSecondStep(key, info, length); 104 | } 105 | 106 | public static byte[] ConvertInt(int number) 107 | { 108 | byte[] output = BitConverter.GetBytes(Convert.ToUInt16(number)); 109 | if (BitConverter.IsLittleEndian) Array.Reverse(output); 110 | return output; 111 | } 112 | 113 | public static byte[] CreateInfoChunk(string type, byte[] recipientPublicKey, byte[] senderPublicKey) 114 | { 115 | List output = new List(); 116 | output.AddRange(Encoding.UTF8.GetBytes($"Content-Encoding: {type}\0P-256\0")); 117 | output.AddRange(ConvertInt(recipientPublicKey.Length)); 118 | output.AddRange(recipientPublicKey); 119 | output.AddRange(ConvertInt(senderPublicKey.Length)); 120 | output.AddRange(senderPublicKey); 121 | return output.ToArray(); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /WebPush.NetCore/Util/JWSSigner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | using Org.BouncyCastle.Crypto.Parameters; 7 | using Org.BouncyCastle.Crypto.Signers; 8 | using Org.BouncyCastle.Math; 9 | using Org.BouncyCastle.Security; 10 | 11 | namespace WebPush.Util 12 | { 13 | public class JWSSigner 14 | { 15 | private readonly ECPrivateKeyParameters _privateKey; 16 | 17 | public JWSSigner(ECPrivateKeyParameters privateKey) 18 | { 19 | _privateKey = privateKey; 20 | } 21 | 22 | /// 23 | /// Generates a Jws Signature. 24 | /// 25 | /// 26 | /// 27 | /// 28 | public string GenerateSignature(Dictionary header, Dictionary payload) 29 | { 30 | 31 | string securedInput = SecureInput(header, payload); 32 | byte[] message = Encoding.UTF8.GetBytes(securedInput); 33 | 34 | SHA256 sha256Hasher = SHA256.Create(); 35 | byte[] hashedMessage = sha256Hasher.ComputeHash(message); 36 | 37 | ECDsaSigner signer = new ECDsaSigner(); 38 | signer.Init(true, _privateKey); 39 | BigInteger[] results = signer.GenerateSignature(hashedMessage); 40 | 41 | // Concated to create signature 42 | var a = results[0].ToByteArrayUnsigned(); 43 | var b = results[1].ToByteArrayUnsigned(); 44 | 45 | // a,b are required to be exactly the same length of bytes 46 | if (a.Length != b.Length) 47 | { 48 | int largestLength = Math.Max(a.Length, b.Length); 49 | a = ByteArrayPadLeft(a, largestLength); 50 | b = ByteArrayPadLeft(b, largestLength); 51 | } 52 | 53 | string signature = UrlBase64.Encode(a.Concat(b).ToArray()); 54 | return String.Format("{0}.{1}", securedInput, signature); 55 | } 56 | 57 | private static string SecureInput(Dictionary header, Dictionary payload) 58 | { 59 | string encodeHeader = UrlBase64.Encode(Encoding.UTF8.GetBytes(Newtonsoft.Json.JsonConvert.SerializeObject(header))); 60 | string encodePayload = UrlBase64.Encode(Encoding.UTF8.GetBytes(Newtonsoft.Json.JsonConvert.SerializeObject(payload))); 61 | 62 | return String.Format("{0}.{1}", encodeHeader, encodePayload); 63 | } 64 | 65 | private static byte[] ByteArrayPadLeft(byte[] src, int size) 66 | { 67 | byte[] dst = new byte[size]; 68 | var startAt = dst.Length - src.Length; 69 | Array.Copy(src, 0, dst, startAt, src.Length); 70 | return dst; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /WebPush.NetCore/Util/UrlBase64.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebPush.Util 4 | { 5 | public static class UrlBase64 6 | { 7 | /// 8 | /// Decodes a url-safe base64 string into bytes 9 | /// 10 | /// 11 | /// 12 | public static byte[] Decode(string base64) 13 | { 14 | 15 | base64 = base64.Replace('-', '+').Replace('_', '/'); 16 | 17 | while (base64.Length % 4 != 0) 18 | base64 += "="; 19 | 20 | return Convert.FromBase64String(base64); 21 | } 22 | 23 | /// 24 | /// Encodes bytes into url-safe base64 string 25 | /// 26 | /// 27 | /// 28 | public static string Encode(byte[] data) 29 | { 30 | return Convert.ToBase64String(data).Replace('+', '-').Replace('/', '_').TrimEnd('='); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /WebPush.NetCore/VapidHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Org.BouncyCastle.Crypto; 8 | using Org.BouncyCastle.Crypto.Parameters; 9 | using WebPush.Util; 10 | 11 | namespace WebPush 12 | { 13 | public static class VapidHelper 14 | { 15 | /// 16 | /// Generate vapid keys 17 | /// 18 | /// 19 | public static VapidDetails GenerateVapidKeys() 20 | { 21 | VapidDetails results = new VapidDetails(); 22 | 23 | AsymmetricCipherKeyPair keys = ECKeyHelper.GenerateKeys(); 24 | byte[] publicKey = ((ECPublicKeyParameters)keys.Public).Q.GetEncoded(false); 25 | byte[] privateKey = ((ECPrivateKeyParameters)keys.Private).D.ToByteArrayUnsigned(); 26 | 27 | results.PublicKey = UrlBase64.Encode(publicKey); 28 | results.PrivateKey = UrlBase64.Encode(privateKey); 29 | 30 | return results; 31 | } 32 | 33 | /// 34 | /// This method takes the required VAPID parameters and returns the required 35 | /// header to be added to a Web Push Protocol Request. 36 | /// 37 | /// This must be the origin of the push service. 38 | /// This should be a URL or a 'mailto:' email address 39 | /// The VAPID public key as a base64 encoded string 40 | /// The VAPID private key as a base64 encoded string 41 | /// The expiration of the VAPID JWT. 42 | /// A dictionary of header key/value pairs. 43 | public static Dictionary GetVapidHeaders(string audience, string subject, string publicKey, string privateKey, long expiration = -1) 44 | { 45 | ValidateAudience(audience); 46 | ValidateSubject(subject); 47 | ValidatePublicKey(publicKey); 48 | ValidatePrivateKey(privateKey); 49 | 50 | byte[] decodedPrivateKey = UrlBase64.Decode(privateKey); 51 | 52 | if (expiration == -1) 53 | { 54 | expiration = UnixTimeNow() + 43200; 55 | } 56 | 57 | Dictionary header = new Dictionary(); 58 | header.Add("typ", "JWT"); 59 | header.Add("alg", "ES256"); 60 | 61 | Dictionary jwtPayload = new Dictionary(); 62 | jwtPayload.Add("aud", audience); 63 | jwtPayload.Add("exp", expiration); 64 | jwtPayload.Add("sub", subject); 65 | 66 | ECPrivateKeyParameters signingKey = ECKeyHelper.GetPrivateKey(decodedPrivateKey); 67 | 68 | JWSSigner signer = new JWSSigner(signingKey); 69 | string token = signer.GenerateSignature(header, jwtPayload); 70 | 71 | Dictionary results = new Dictionary(); 72 | results.Add("Authorization", "WebPush " + token); 73 | results.Add("Crypto-Key", "p256ecdsa=" + publicKey); 74 | 75 | return results; 76 | } 77 | 78 | public static void ValidateAudience(string audience) 79 | { 80 | if (String.IsNullOrEmpty(audience)) 81 | { 82 | throw new ArgumentException(@"No audience could be generated for VAPID."); 83 | } 84 | 85 | if (audience.Length == 0) 86 | { 87 | throw new ArgumentException(@"The audience value must be a string containing the origin of a push service. " + audience); 88 | } 89 | 90 | if (!Uri.IsWellFormedUriString(audience, UriKind.Absolute)) 91 | { 92 | throw new ArgumentException(@"VAPID audience is not a url."); 93 | } 94 | 95 | } 96 | 97 | public static void ValidateSubject(string subject) 98 | { 99 | if (String.IsNullOrEmpty(subject)) 100 | { 101 | throw new ArgumentException(@"A subject is required"); 102 | } 103 | 104 | if (subject.Length == 0) 105 | { 106 | throw new ArgumentException(@"The subject value must be a string containing a url or mailto: address."); 107 | } 108 | 109 | if (!subject.StartsWith("mailto:")) 110 | { 111 | if (!Uri.IsWellFormedUriString(subject, UriKind.Absolute)) 112 | { 113 | throw new ArgumentException(@"Subject is not a valid URL or mailto address"); 114 | } 115 | } 116 | } 117 | 118 | public static void ValidatePublicKey(string publicKey) 119 | { 120 | if (String.IsNullOrEmpty(publicKey)) 121 | { 122 | throw new ArgumentException(@"Valid public key not set"); 123 | } 124 | 125 | byte[] decodedPublicKey = UrlBase64.Decode(publicKey); 126 | 127 | if (decodedPublicKey.Length != 65) 128 | { 129 | throw new ArgumentException(@"Vapid public key must be 65 characters long when decoded"); 130 | } 131 | } 132 | 133 | 134 | public static void ValidatePrivateKey(string privateKey) 135 | { 136 | if (String.IsNullOrEmpty(privateKey)) 137 | { 138 | throw new ArgumentException(@"Valid private key not set"); 139 | } 140 | 141 | byte[] decodedPrivateKey = UrlBase64.Decode(privateKey); 142 | 143 | 144 | if (decodedPrivateKey.Length != 32) 145 | { 146 | throw new ArgumentException(@"Vapid private key should be 32 bytes long when decoded."); 147 | } 148 | } 149 | 150 | private static long UnixTimeNow() 151 | { 152 | TimeSpan timeSpan = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0)); 153 | return (long)timeSpan.TotalSeconds; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /WebPush.NetCore/WebPush.NetCore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WebPush.NetCore 5 | netcoreapp1.0 6 | WebPush-NetCore 7 | 1.0.1 8 | vip30 9 | Webpush library for .NetCore 10 | false 11 | First release 12 | Copyright 2015 - 2017 Vip30 13 | Push Notifcation GCM FCM 14 | https://github.com/vip30/WebPush-NetCore 15 | https://raw.githubusercontent.com/vip30/WebPush-NetCore/master/LICENSE 16 | https://github.com/vip30/WebPush-NetCore 17 | 1.0.1 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /WebPush.NetCore/WebPushClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Net.Http.Headers; 5 | using System.Threading.Tasks; 6 | using WebPush.Util; 7 | 8 | namespace WebPush 9 | { 10 | public class WebPushClient 11 | { 12 | // default TTL is 4 weeks. 13 | const int DefaultTtl = 2419200; 14 | 15 | private string _gcmAPIKey = null; 16 | private HttpClient _httpClient = null; 17 | private VapidDetails _vapidDetails = null; 18 | 19 | public WebPushClient() 20 | { 21 | 22 | } 23 | 24 | protected HttpClient httpClient 25 | { 26 | get 27 | { 28 | if (_httpClient == null) 29 | { 30 | _httpClient = new HttpClient(); 31 | } 32 | 33 | return _httpClient; 34 | } 35 | } 36 | 37 | /// 38 | /// When sending messages to a GCM endpoint you need to set the GCM API key 39 | /// by either calling setGCMAPIKey() or passing in the API key as an option 40 | /// to sendNotification() 41 | /// 42 | /// The API key to send with the GCM request. 43 | public void SetGCMAPIKey(string apiKey) 44 | { 45 | if (apiKey == null) 46 | { 47 | _gcmAPIKey = null; 48 | return; 49 | } 50 | 51 | if (String.IsNullOrEmpty(apiKey)) 52 | { 53 | throw new ArgumentException(@"The GCM API Key should be a non-empty string or null."); 54 | } 55 | 56 | _gcmAPIKey = apiKey; 57 | } 58 | 59 | /// 60 | /// When marking requests where you want to define VAPID details, call this method 61 | /// before sendNotifications() or pass in the details and options to 62 | /// sendNotification. 63 | /// 64 | /// 65 | public void SetVapidDetails(VapidDetails vapidDetails) 66 | { 67 | VapidHelper.ValidateSubject(vapidDetails.Subject); 68 | VapidHelper.ValidatePublicKey(vapidDetails.PublicKey); 69 | VapidHelper.ValidatePrivateKey(vapidDetails.PrivateKey); 70 | 71 | _vapidDetails = vapidDetails; 72 | } 73 | 74 | /// 75 | /// When marking requests where you want to define VAPID details, call this method 76 | /// before sendNotifications() or pass in the details and options to 77 | /// sendNotification. 78 | /// 79 | /// This must be either a URL or a 'mailto:' address 80 | /// The public VAPID key as a base64 encoded string 81 | /// The private VAPID key as a base64 encoded string 82 | public void SetVapidDetails(string subject, string publicKey, string privateKey) 83 | { 84 | 85 | SetVapidDetails(new VapidDetails(subject, publicKey, privateKey)); 86 | } 87 | 88 | /// 89 | /// To get a request without sending a push notification call this method. 90 | /// 91 | /// This method will throw an ArgumentException if there is an issue with the input. 92 | /// 93 | /// The PushSubscription you wish to send the notification to. 94 | /// The payload you wish to send to the user 95 | /// Options for the GCM API key and vapid keys can be passed in if they are unique for each notification. 96 | /// A HttpRequestMessage object that can be sent. 97 | public HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string payload, Dictionary options = null) 98 | { 99 | if (!Uri.IsWellFormedUriString(subscription.Endpoint, UriKind.Absolute)) 100 | { 101 | throw new ArgumentException(@"You must pass in a subscription with at least a valid endpoint"); 102 | } 103 | 104 | HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, subscription.Endpoint); 105 | 106 | if (!String.IsNullOrEmpty(payload) && (String.IsNullOrEmpty(subscription.Auth) || String.IsNullOrEmpty(subscription.P256DH))) 107 | { 108 | throw new ArgumentException(@"To send a message with a payload, the subscription must have 'auth' and 'p256dh' keys."); 109 | } 110 | 111 | string currentGCMAPiKey = _gcmAPIKey; 112 | VapidDetails currentVapidDetails = _vapidDetails; 113 | int timeToLive = DefaultTtl; 114 | Dictionary extraHeaders = new Dictionary(); 115 | 116 | if (options != null) 117 | { 118 | List validOptionsKeys = new List { "headers", "gcmAPIKey", "vapidDetails", "TTL" }; 119 | foreach (string key in options.Keys) 120 | { 121 | if (!validOptionsKeys.Contains(key)) 122 | { 123 | throw new ArgumentException(key + " is an invalid options. The valid options are" + String.Join(",", validOptionsKeys)); 124 | } 125 | } 126 | 127 | 128 | if (options.ContainsKey("headers")) 129 | { 130 | Dictionary headers = options["headers"] as Dictionary; 131 | if (headers == null) 132 | { 133 | throw new ArgumentException("options.headers must be of type Dictionary"); 134 | } 135 | 136 | extraHeaders = headers; 137 | } 138 | 139 | if (options.ContainsKey("gcmAPIKey")) 140 | { 141 | string gcmAPIKey = options["gcmAPIKey"] as string; 142 | if (gcmAPIKey == null) 143 | { 144 | throw new ArgumentException("options.gcmAPIKey must be of type string"); 145 | } 146 | 147 | currentGCMAPiKey = gcmAPIKey; 148 | } 149 | 150 | if (options.ContainsKey("vapidDetails")) 151 | { 152 | VapidDetails vapidDetails = options["vapidDetails"] as VapidDetails; 153 | if (vapidDetails == null) 154 | { 155 | throw new ArgumentException("options.vapidDetails must be of type VapidDetails"); 156 | } 157 | 158 | currentVapidDetails = vapidDetails; 159 | } 160 | 161 | if (options.ContainsKey("TTL")) 162 | { 163 | int? ttl = options["TTL"] as int?; 164 | if (ttl == null) 165 | { 166 | throw new ArgumentException("options.TTL must be of type int"); 167 | } 168 | 169 | //at this stage ttl cannot be null. 170 | timeToLive = (int)ttl; 171 | } 172 | } 173 | 174 | string cryptoKeyHeader = null; 175 | request.Headers.Add("TTL", timeToLive.ToString()); 176 | 177 | foreach (KeyValuePair header in extraHeaders) 178 | { 179 | request.Headers.Add(header.Key, header.Value.ToString()); 180 | } 181 | 182 | if (!String.IsNullOrEmpty(payload)) 183 | { 184 | if (String.IsNullOrEmpty(subscription.P256DH) || String.IsNullOrEmpty(subscription.Auth)) 185 | { 186 | throw new ArgumentException(@"Unable to send a message with payload to this subscription since it doesn't have the required encryption key"); 187 | } 188 | 189 | EncryptionResult encryptedPayload = Encryptor.Encrypt(subscription.P256DH, subscription.Auth, payload); 190 | 191 | request.Content = new ByteArrayContent(encryptedPayload.Payload); 192 | request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); 193 | request.Content.Headers.ContentLength = encryptedPayload.Payload.Length; 194 | request.Content.Headers.ContentEncoding.Add("aesgcm"); 195 | request.Headers.Add("Encryption", "salt=" + encryptedPayload.Base64EncodeSalt()); 196 | cryptoKeyHeader = @"dh=" + encryptedPayload.Base64EncodePublicKey(); 197 | } 198 | else 199 | { 200 | request.Content = new ByteArrayContent(new byte[0]); 201 | request.Content.Headers.ContentLength = 0; 202 | } 203 | 204 | bool isGCM = subscription.Endpoint.StartsWith(@"https://android.googleapis.com/gcm/send"); 205 | if (isGCM) 206 | { 207 | if (!String.IsNullOrEmpty(currentGCMAPiKey)) 208 | { 209 | request.Headers.TryAddWithoutValidation("Authorization", "key=" + currentGCMAPiKey); 210 | } 211 | } 212 | else if (currentVapidDetails != null) 213 | { 214 | 215 | Uri uri = new Uri(subscription.Endpoint); 216 | string audience = uri.Scheme + "://" + uri.Host; 217 | 218 | Dictionary vapidHeaders = VapidHelper.GetVapidHeaders(audience, currentVapidDetails.Subject, currentVapidDetails.PublicKey, currentVapidDetails.PrivateKey); 219 | request.Headers.Add(@"Authorization", vapidHeaders["Authorization"]); 220 | if (String.IsNullOrEmpty(cryptoKeyHeader)) 221 | { 222 | cryptoKeyHeader = vapidHeaders["Crypto-Key"]; 223 | } 224 | else 225 | { 226 | cryptoKeyHeader += @";" + vapidHeaders["Crypto-Key"]; 227 | } 228 | } 229 | 230 | request.Headers.Add("Crypto-Key", cryptoKeyHeader); 231 | return request; 232 | } 233 | 234 | /// 235 | /// To send a push notification call this method with a subscription, optional payload and any options 236 | /// Will exception is unsuccessful 237 | /// 238 | /// The PushSubscription you wish to send the notification to. 239 | /// The payload you wish to send to the user 240 | /// Options for the GCM API key and vapid keys can be passed in if they are unique for each notification. 241 | public void SendNotification(PushSubscription subscription, string payload = null, Dictionary options = null) 242 | { 243 | SendNotificationAsync(subscription, payload, options).Wait(); 244 | } 245 | 246 | /// 247 | /// To send a push notification call this method with a subscription, optional payload and any options 248 | /// Will exception is unsuccessful 249 | /// 250 | /// The PushSubscription you wish to send the notification to. 251 | /// The payload you wish to send to the user 252 | /// The vapid details for the notification. 253 | public void SendNotification(PushSubscription subscription, string payload, VapidDetails vapidDetails) 254 | { 255 | SendNotificationAsync(subscription, payload, vapidDetails).Wait(); 256 | } 257 | 258 | /// 259 | /// To send a push notification call this method with a subscription, optional payload and any options 260 | /// Will exception is unsuccessful 261 | /// 262 | /// The PushSubscription you wish to send the notification to. 263 | /// The payload you wish to send to the user 264 | /// The GCM API key 265 | public void SendNotification(PushSubscription subscription, string payload, string gcmAPIKey) 266 | { 267 | SendNotificationAsync(subscription, payload, gcmAPIKey).Wait(); 268 | } 269 | 270 | /// 271 | /// To send a push notification asyncronously call this method with a subscription, optional payload and any options 272 | /// Will exception is unsuccessful 273 | /// 274 | /// The PushSubscription you wish to send the notification to. 275 | /// The payload you wish to send to the user 276 | /// Options for the GCM API key and vapid keys can be passed in if they are unique for each notification. 277 | public async Task SendNotificationAsync(PushSubscription subscription, string payload = null, Dictionary options = null) 278 | { 279 | 280 | HttpRequestMessage request = GenerateRequestDetails(subscription, payload, options); 281 | 282 | HttpResponseMessage response = await httpClient.SendAsync(request); 283 | 284 | if (response.StatusCode != System.Net.HttpStatusCode.Created) //201 285 | { 286 | throw new WebPushException(@"Received unexpected response code", response.StatusCode, response.Headers, subscription); 287 | } 288 | 289 | } 290 | 291 | /// 292 | /// To send a push notification asyncronously call this method with a subscription, optional payload and any options 293 | /// Will exception is unsuccessful 294 | /// 295 | /// The PushSubscription you wish to send the notification to. 296 | /// The payload you wish to send to the user 297 | /// The vapid details for the notification. 298 | public async Task SendNotificationAsync(PushSubscription subscription, string payload, VapidDetails vapidDetails) 299 | { 300 | Dictionary options = new Dictionary(); 301 | options["vapidDetails"] = vapidDetails; 302 | await SendNotificationAsync(subscription, payload, options); 303 | } 304 | 305 | /// 306 | /// To send a push notification asyncronously call this method with a subscription, optional payload and any options 307 | /// Will exception is unsuccessful 308 | /// 309 | /// The PushSubscription you wish to send the notification to. 310 | /// The payload you wish to send to the user 311 | /// The GCM API key 312 | public async Task SendNotificationAsync(PushSubscription subscription, string payload, string gcmAPIKey) 313 | { 314 | Dictionary options = new Dictionary(); 315 | options["gcmAPIKey"] = gcmAPIKey; 316 | await SendNotificationAsync(subscription, payload, options); 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | dotnet restore && dotnet build --------------------------------------------------------------------------------